Signing and verification#
See Signing & verification for keys and the signature format.
- verify_envelope(key: VerifyKey, envelope: Any, *, max_age_secs: int, slot: str, skew_secs: int = 5, now: int | None = None) None[source]#
Verify a received envelope.
The checks run in order, and the first failure raises:
the key is usable (
invalid verification key);vis 2 (unsupported envelope version);tsis a non-negative integer,slotandhtmlare strings,cssis absent or a string (malformed envelope);sigis present (envelope is not signed) and lowercase hex (malformed signature);tsis at mostskew_secsahead ofnow(envelope timestamp is in the future) and at mostmax_age_secsold (envelope expired);the envelope is for
slot(slot mismatch);the signature over
signature_message()matches, compared in constant time (bad signature).
- Parameters:
key – The verification key, see
VerifyKey.envelope – An
Envelopeor the parsed JSONdict.max_age_secs – Oldest accepted envelope, in seconds.
slot – The slot the envelope must be for.
skew_secs – Tolerance for a timestamp ahead of the verifier’s clock.
now – The current Unix time (defaults to
time.time()).
- Returns:
None – When the envelope is valid.
- Raises:
AnzaError –
CRYPTO, with the failed check as the message.
Example
>>> from anza import Envelope, VerifyKey, hmac, verify_envelope >>> secret = "a-secret-of-at-least-32-bytes!!!" >>> env = Envelope(slot="feed", ts=1700000000, html="<p>Hi</p>") >>> env.sig = hmac.sign(secret, env.message()) >>> key = VerifyKey.hmac(secret) >>> verify_envelope(key, env.to_dict(), max_age_secs=60, slot="feed", now=1700000010) >>> verify_envelope(key, env.to_dict(), max_age_secs=60, slot="feed", now=1700000100) Traceback (most recent call last): ... anza.errors.AnzaError: [Anza:CRYPTO] envelope expired
Warning
A valid signature alone does not stop replays: always pass the slot you expect and a
max_age_secsthat fits the use.
- class VerifyKey(mode: str, secret: str | bytes | None = None, public_key: Any = None)[source]#
The key
verify_envelope()checks a signature with.Build it with
hmac()ored25519(). The key is validated when it is used: a short HMAC secret or a malformed public key fails the verification withCRYPTOinvalid verification key.reprnever prints the key.Example
>>> from anza import VerifyKey >>> VerifyKey.hmac("a-secret-of-at-least-32-bytes!!!") VerifyKey(mode='hmac', key=<redacted>)
- signature_message(ts: int, slot: str, html: str, css: str | None = None) bytes[source]#
The exact bytes a v2 envelope signature covers.
Every field is length-prefixed, so no two different envelopes produce the same message.
- Parameters:
ts – The envelope timestamp, in Unix seconds.
slot – The envelope slot.
html – The envelope HTML.
css – The envelope CSS;
Nonesigns as the empty string.
- Returns:
bytes –
anza-sig-v2\nfollowed by<len>:<value>\nforts,slot,htmlandcss.
Example
>>> from anza import signature_message >>> signature_message(1700000000, "feed", "<p>Hi</p>") b'anza-sig-v2\n10:1700000000\n4:feed\n9:<p>Hi</p>\n0:\n'
- ENVELOPE_VERSION = 2#
Envelope wire-format version produced and accepted by this engine.
- class Signer[source]#
A validated signing key, built once at setup.
Setup.run()builds it fromSignOptionswithfrom_options()and stores it asEngine.signer. A running engine never signs with a missing, short or mismatched key, andreprnever prints key material.- mode#
"none","hmac"or"ed25519".
Example
>>> from anza import Signer, SignOptions >>> signer = Signer.from_options(SignOptions(mode="hmac", secret="k" * 32)) >>> signer Signer(mode='hmac', key=<redacted>) >>> sig = signer.sign(b"message") >>> signer.verify(b"message", sig), signer.verify(b"other", sig) (True, False) >>> Signer.from_options(SignOptions(mode="hmac", secret="short")) Traceback (most recent call last): ... anza.errors.AnzaError: [Anza:CRYPTO] HMAC secret must be at least 32 bytes
- classmethod from_options(options: Any) Signer[source]#
Validate
optionsand decode its keys.- Parameters:
options – A
SignOptions;Nonedisables signing.- Returns:
Signer – The validated signer.
- Raises:
AnzaError –
CRYPTOfor an HMAC secret shorter than 32 bytes or not str/bytes, a missing or malformed Ed25519 private key, a public key that does not match it, or an unknown mode.
- sign(message: bytes) str | None[source]#
Sign
message.- Parameters:
message – The bytes to sign, normally
Envelope.message().- Returns:
str | None – The lowercase-hex signature, or
Nonewhen signing is off.
- verify(message: bytes, signature_hex: str) bool[source]#
Check a lowercase-hex signature made by this signer, in constant time.
- Parameters:
message – The signed bytes.
signature_hex – The signature to check.
- Returns:
bool –
Truewhen it matches; alwaysFalsewhen signing is off or the signature is malformed.
anza.hmac#
HMAC-SHA256 signatures in lowercase hex (available as anza.hmac).
- sign(secret: str | bytes, data: str | bytes) str[source]#
HMAC-SHA256 of
dataundersecret, as lowercase hex.This low-level helper does not enforce a key length; envelope signing (
SignOptions) requires a secret of at least 32 bytes.- Parameters:
secret – The key; a
stris encoded as UTF-8.data – The message; a
stris encoded as UTF-8.
- Returns:
str – 64 lowercase hex characters.
Example
>>> from anza import hmac >>> sig = hmac.sign("k" * 32, "hi") >>> sig '1afd7d962ebfcf74a55488bdd29cb42e6da37404b5ed234e30ca6d8831252a84' >>> hmac.verify("k" * 32, "hi", sig), hmac.verify("k" * 32, "hi", sig.upper()) (True, False)
- verify(secret: str | bytes, data: str | bytes, signature_hex: str) bool[source]#
Verify a lowercase-hex HMAC-SHA256 signature in constant time.
- Parameters:
secret – The key; a
stris encoded as UTF-8.data – The signed message.
signature_hex – The signature to check.
- Returns:
bool –
Truewhen it matches. Malformed signatures (wrong length, non-hex, uppercase) returnFalserather than raising.
anza.ed25519#
Ed25519 signing and verification (available as anza.ed25519).
Backed by the cryptography package (constant-time, RFC 8032). Keys are
exactly 32 bytes, 64-char lowercase hex, or cryptography key objects;
anything else is rejected. Signatures are 128 lowercase hex characters.
- keypair() Tuple[bytes, bytes][source]#
Generate a new Ed25519 key pair.
- Returns:
tuple[bytes, bytes] –
(private_seed, public_key), 32 bytes each. Keep the seed secret; give verifiers the public key.
Example
>>> from anza import ed25519 >>> seed, public = ed25519.keypair() >>> len(seed), len(public) (32, 32)
- sign(key: Any, data: str | bytes) str[source]#
Sign
datawith an Ed25519 private key.- Parameters:
key – A 32-byte seed, 64-char lowercase hex, or an
Ed25519PrivateKey.data – The message; a
stris encoded as UTF-8.
- Returns:
str – The signature, 128 lowercase hex characters.
- Raises:
AnzaError –
CRYPTOfor a malformed key.
Example
>>> from anza import ed25519 >>> seed, public = ed25519.keypair() >>> sig = ed25519.sign(seed, "hi") >>> len(sig), ed25519.verify(public, "hi", sig), ed25519.verify(public, "ho", sig) (128, True, False)
- verify(key: Any, data: str | bytes, signature_hex: str) bool[source]#
Verify a lowercase-hex Ed25519 signature.
- Parameters:
key – The public key: 32 bytes, 64-char lowercase hex, or an
Ed25519PublicKey.data – The signed message.
signature_hex – The signature to check.
- Returns:
bool –
Truewhen it is valid. Malformed keys or signatures (wrong length, non-hex, uppercase) returnFalserather than raising.
anza.hkdf#
HKDF-SHA256 key derivation (available as anza.hkdf).
- derive_key(ikm: str | bytes, salt: str | bytes | None = None, info: str | bytes = 'anza-stui-session-v1', keylen: int = 32) bytes[source]#
Derive a key with HKDF-SHA256 (RFC 5869).
Use it to turn one master secret into separate keys per purpose (for example an HMAC envelope secret per service).
- Parameters:
ikm – Input keying material (the master secret).
salt – Optional salt; empty when omitted.
info – Context string that separates the derived keys.
keylen – Length of the derived key, in bytes.
- Returns:
bytes –
keylenbytes of key material.- Raises:
AnzaError –
CRYPTOwhen derivation fails (e.g.keylentoo large).
Example
>>> from anza import hkdf >>> key = hkdf.derive_key("master secret", info="feed-envelopes") >>> len(key) 32
anza.digest#
SHA-256 digests (available as anza.digest).