MishkaCmsCore.Runtime.PageBuilder.CodeSignature (Mishka CMS Core v0.0.1)

Copy Markdown View Source

An HMAC over each stored snippet, so code that did not come through this application does not run.

What it buys, and it is one thing

Every future arbitrary-database-write bug in this CMS — an injection, a mis-scoped Ash action, a restored dump somebody tampered with — currently ends in remote code execution, because whatever is in events, infos or helpers is compiled into the module that serves the site. With signing on, such a write produces code that does not run: the signature will not match, and the compiler emits an inert clause instead.

It is not a permission check and does not replace one. The resource's policies decide who may write; this decides whether what is stored was written by this application at all.

The page still renders

A snippet whose signature is missing or wrong is replaced by a clause that does nothing, and the rest of the document compiles and serves as normal. The alternative — refusing to compile — turns a tampered helper into a site-wide outage, which hands whoever tampered with it a better outcome than they had.

The cost, stated plainly

A database restored into another environment runs no page code until it is re-signed. The key is per-deployment, so a production dump loaded into staging carries signatures staging cannot verify. That is the feature working, and it will read as a bug the first time somebody hits it — mix mishka.resign_code is the answer, and the log line says so by name.

Off unless a key is configured

No key, no signing and no verification: enabled?/0 is false, and everything behaves exactly as it did before this module existed. A deployment opts in by setting one, which is the only way to add this to an existing database without every page stopping at once.

config :mishka_cms_core, :code_signing_key, System.get_env("CODE_SIGNING_KEY")

Summary

Functions

Whether this deployment signs and verifies stored code.

One snippet with its signature attached — what the resources store.

The signature for one snippet's code, or nil when signing is off.

Whether a stored snippet carries a signature this deployment made — what the compiler asks before it turns the snippet into a clause.

Whether signature is this deployment's signature for code.

Functions

enabled?()

@spec enabled?() :: boolean()

Whether this deployment signs and verifies stored code.

False when no key is configured, which is the default and which makes every other function here a no-op — an existing database keeps working until somebody deliberately turns this on.

put(snippet)

@spec put(map()) :: map()

One snippet with its signature attached — what the resources store.

A map with string keys keeps whatever it had; the key is added under the same kind of key the map already uses, so a snippet that arrived from JSON:API and one that arrived from the editor come out looking the same.

sign(code)

@spec sign(String.t() | nil) :: String.t() | nil

The signature for one snippet's code, or nil when signing is off.

Normalized first, because a save that only changes indentation must not invalidate the signature and, more importantly, because the compiler has to compute the same digest from the same text — \r\n from a paste and trailing spaces from an editor are the two ways that goes wrong.

trusted?(snippet)

@spec trusted?(map()) :: boolean()

Whether a stored snippet carries a signature this deployment made — what the compiler asks before it turns the snippet into a clause.

valid?(code, signature)

@spec valid?(String.t() | nil, String.t() | nil) :: boolean()

Whether signature is this deployment's signature for code.

Answers true when signing is off — with no key there is nothing to verify, and the compiler must behave exactly as it did before. Compared in constant time, because a signature is a secret-derived value and a byte-at-a-time comparison leaks where it stopped matching.