Unix time in seconds.
Target name.
Fragment HTML.
Optionalcss: string | null
Optional CSS; absent is the empty string.
The UTF-8 message bytes.
Signature v2, shared by all engines (see the Engine Contract):
message = "anza-sig-v2\n" field(ts) field(slot) field(html) field(css)
field(x) = <decimal UTF-8 byte length of x> ":" x "\n"
ts is written in decimal and an absent css is the empty string. Every
field is length-prefixed, so two different envelopes never share a message,
and changing the timestamp, slot, HTML or CSS breaks the signature. The
signature (sig) is the lowercase hex HMAC-SHA256 or Ed25519 signature of
these bytes.
To verify in the browser yourself (the Anza browser library does not),
rebuild the same bytes and call crypto.subtle.verify.
import { signatureMessage } from '@anzaui/engine';
signatureMessage(1, 'a:b', 'c').toString();
// 'anza-sig-v2\n1:1\n3:a:b\n1:c\n0:\n'
const enc = new TextEncoder();
const field = (x) => `${enc.encode(x).length}:${x}\n`;
const message = (env) =>
enc.encode('anza-sig-v2\n' + field(String(env.ts)) + field(env.slot) + field(env.html) + field(env.css ?? ''));
async function verify(env, publicKeyBytes, slot, maxAgeSecs = 60) {
if (env.v !== 2 || env.slot !== slot || !/^[0-9a-f]{128}$/.test(env.sig ?? '')) return false;
const now = Math.floor(Date.now() / 1000);
if (env.ts > now + 5 || now - env.ts > maxAgeSecs) return false;
const key = await crypto.subtle.importKey('raw', publicKeyBytes, { name: 'Ed25519' }, false, ['verify']);
const sig = Uint8Array.from(env.sig.match(/../g), (h) => parseInt(h, 16));
return crypto.subtle.verify({ name: 'Ed25519' }, key, sig, message(env));
}
The exact bytes a v2 envelope signature covers.