Skip to content

memory.sharing.signing

memory.sharing.signing

Ed25519 detached signing for identities — a primitive alongside age.

age (via pyrage, see :mod:~memory.sharing.crypto) provides encryption only: it has no notion of signing or authenticating a message. So authenticity — proving a message was produced by the holder of a given identity's private key — needs a separate primitive. This module supplies it with Ed25519 (via the cryptography library; we never hand-roll key material or the signature scheme).

An identity therefore carries two independent keypairs:

  • an age X25519 keypair for encryption (:mod:~memory.sharing.identity), and
  • an Ed25519 keypair for signing (this module).

The API is intentionally tiny and typed:

  • :func:generate_signing_keypair — mint a fresh Ed25519 keypair.
  • :func:sign — produce a detached signature over a message.
  • :func:verify — check a detached signature; returns False on ANY failure and NEVER raises to the caller.

All keys and signatures are exchanged as URL-safe base64 of the raw bytes (the 32-byte public/private key seeds and the 64-byte signature), so they are safe to embed in YAML/JSON without extra escaping.

generate_signing_keypair

generate_signing_keypair() -> tuple[str, str]

Generate a fresh Ed25519 keypair.

Returns (public_b64, private_b64) — the URL-safe base64 encodings of the raw 32-byte public key and the raw 32-byte private key seed.

Source code in memory/sharing/signing.py
def generate_signing_keypair() -> tuple[str, str]:
    """Generate a fresh Ed25519 keypair.

    Returns ``(public_b64, private_b64)`` — the URL-safe base64 encodings of the
    raw 32-byte public key and the raw 32-byte private key seed.
    """
    private = ed25519.Ed25519PrivateKey.generate()
    private_raw = private.private_bytes(
        Encoding.Raw, PrivateFormat.Raw, NoEncryption()
    )
    public_raw = private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
    return _b64encode(public_raw), _b64encode(private_raw)

sign

sign(message: bytes, private_key_b64: str) -> str

Sign message with the base64-encoded Ed25519 private key.

Returns the URL-safe base64 of the raw 64-byte detached signature. Raises if private_key_b64 is malformed (a caller signing with its own key should know the key is valid); use :func:verify for the never-raises path.

A None or empty key is rejected up front with a clear :class:ValueError — signing is the TRUSTED path (unlike :func:verify, which never raises), and a missing key is a programmer error that must surface explicitly rather than as an opaque decode/type failure deep in the cryptography internals.

Source code in memory/sharing/signing.py
def sign(message: bytes, private_key_b64: str) -> str:
    """Sign ``message`` with the base64-encoded Ed25519 private key.

    Returns the URL-safe base64 of the raw 64-byte detached signature. Raises if
    ``private_key_b64`` is malformed (a caller signing with its own key should
    know the key is valid); use :func:`verify` for the never-raises path.

    A ``None`` or empty key is rejected up front with a clear
    :class:`ValueError` — signing is the TRUSTED path (unlike :func:`verify`,
    which never raises), and a missing key is a programmer error that must surface
    explicitly rather than as an opaque decode/type failure deep in the
    ``cryptography`` internals.
    """
    if not private_key_b64:
        raise ValueError("signing private key required")
    private_raw = base64.urlsafe_b64decode(private_key_b64)
    private = ed25519.Ed25519PrivateKey.from_private_bytes(private_raw)
    return _b64encode(private.sign(bytes(message)))

verify

verify(message: bytes, signature: str | bytes, public_key_b64: str) -> bool

Return whether signature is a valid Ed25519 signature over message.

signature may be a URL-safe base64 str (as produced by :func:sign) or the raw signature bytes themselves. Verification is delegated to the cryptography library (constant-time against the signature). Returns False on ANY failure — bad base64, a wrong-length key or signature, a signature that does not match, or any type error — and NEVER raises to the caller.

Source code in memory/sharing/signing.py
def verify(message: bytes, signature: str | bytes, public_key_b64: str) -> bool:
    """Return whether ``signature`` is a valid Ed25519 signature over ``message``.

    ``signature`` may be a URL-safe base64 ``str`` (as produced by :func:`sign`)
    or the raw signature ``bytes`` themselves. Verification is delegated to the
    ``cryptography`` library (constant-time against the signature). Returns
    ``False`` on ANY failure — bad base64, a wrong-length key or signature, a
    signature that does not match, or any type error — and NEVER raises to the
    caller.
    """
    try:
        public_raw = base64.urlsafe_b64decode(public_key_b64)
        signature_raw = (
            base64.urlsafe_b64decode(signature)
            if isinstance(signature, str)
            else bytes(signature)
        )
        public = ed25519.Ed25519PublicKey.from_public_bytes(public_raw)
        # public.verify() raises InvalidSignature on mismatch; treat every
        # failure (bad key, bad base64, wrong length, type error) as invalid.
        public.verify(signature_raw, bytes(message))
        return True
    except Exception:
        return False