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:

  1. the key is usable (invalid verification key);

  2. v is 2 (unsupported envelope version);

  3. ts is a non-negative integer, slot and html are strings, css is absent or a string (malformed envelope);

  4. sig is present (envelope is not signed) and lowercase hex (malformed signature);

  5. ts is at most skew_secs ahead of now (envelope timestamp is in the future) and at most max_age_secs old (envelope expired);

  6. the envelope is for slot (slot mismatch);

  7. the signature over signature_message() matches, compared in constant time (bad signature).

Parameters:
  • key – The verification key, see VerifyKey.

  • envelope – An Envelope or the parsed JSON dict.

  • 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_secs that 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() or ed25519(). The key is validated when it is used: a short HMAC secret or a malformed public key fails the verification with CRYPTO invalid verification key. repr never prints the key.

Example

>>> from anza import VerifyKey
>>> VerifyKey.hmac("a-secret-of-at-least-32-bytes!!!")
VerifyKey(mode='hmac', key=<redacted>)
mode: str#

"hmac" or "ed25519".

secret: str | bytes | None = None#

The shared HMAC secret.

public_key: Any = None#

The Ed25519 public key.

classmethod hmac(secret: str | bytes) → VerifyKey[source]#

A key for HMAC-SHA256 envelopes.

Parameters:

secret – The shared secret, at least 32 bytes.

classmethod ed25519(public_key: Any) → VerifyKey[source]#

A key for Ed25519 envelopes.

Parameters:

public_key – 32 bytes, 64-char lowercase hex, or an Ed25519PublicKey.

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; None signs as the empty string.

Returns:

bytes – anza-sig-v2\n followed by <len>:<value>\n for ts, slot, html and css.

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 from SignOptions with from_options() and stores it as Engine.signer. A running engine never signs with a missing, short or mismatched key, and repr never 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 options and decode its keys.

Parameters:

options – A SignOptions; None disables signing.

Returns:

Signer – The validated signer.

Raises:

AnzaError – CRYPTO for 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 None when 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 – True when it matches; always False when signing is off or the signature is malformed.

property public_key: bytes | None#

The raw 32-byte Ed25519 public key that verifies this signer’s signatures.

anza.hmac#

HMAC-SHA256 signatures in lowercase hex (available as anza.hmac).

sign(secret: str | bytes, data: str | bytes) → str[source]#

HMAC-SHA256 of data under secret, 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 str is encoded as UTF-8.

  • data – The message; a str is 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 str is encoded as UTF-8.

  • data – The signed message.

  • signature_hex – The signature to check.

Returns:

bool – True when it matches. Malformed signatures (wrong length, non-hex, uppercase) return False rather 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 data with an Ed25519 private key.

Parameters:
  • key – A 32-byte seed, 64-char lowercase hex, or an Ed25519PrivateKey.

  • data – The message; a str is encoded as UTF-8.

Returns:

str – The signature, 128 lowercase hex characters.

Raises:

AnzaError – CRYPTO for 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 – True when it is valid. Malformed keys or signatures (wrong length, non-hex, uppercase) return False rather 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 – keylen bytes of key material.

Raises:

AnzaError – CRYPTO when derivation fails (e.g. keylen too 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).

hash(data: str | bytes) → bytes[source]#

SHA-256 of data (a str is encoded as UTF-8).

Example

>>> from anza import digest
>>> digest.hash("hi")[:4]
b'\x8fCCF'
hex(data: str | bytes) → str[source]#

Lowercase-hex SHA-256 of data (a str is encoded as UTF-8).

This is the digest of Template.digest.

Example

>>> from anza import digest
>>> digest.hex("hi")
'8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4'