NORAEarly Access

Part II — CS Building Blocks · Chapter 13

Digital Signatures (Ed25519)

Digital Signatures (Ed25519)

A hash proves the bytes. A signature proves who committed to the bytes.

Prerequisites

Before reading this chapter, you should be comfortable with: Chapter 5 (Hashing). Signature verification requires understanding what is being signed — the chain_hash and PAE construction only make sense if you know what SHA-256 guarantees.

A hash is a lock with no key. You can hand anyone the combination and they can verify the lock clicked shut — but no one knows who turned the dial. A digital signature adds the missing piece: cryptographic proof that a specific key-holder deliberately committed to those specific bytes, at that specific time, and that no one else could have produced that commitment.

Canon uses Ed25519 signatures at one precise moment in the attestation lifecycle: after the chain hash is computed and before the attestation leaves the issuer's control. From that moment forward, any recipient with the issuer's public key — a judge's forensic expert, opposing counsel's consultant, a journalist running the reference verifier — can independently confirm two things. First, that the attestation has not been altered. Second, that the alterer would have needed the issuer's private key to reissue a valid signature over the altered content. The second property is what a hash alone cannot give you.

At a glance

  • As of Canon v0.2.0, signatures use DSSE (Dead Simple Signing Envelope), an IETF/CNCF standard used by SLSA, in-toto, and TUF. Bare Ed25519 over raw bytes is no longer the canonical format.
  • DSSE encodes both the content type and the payload length in a Pre-Authentication Encoding (PAE) before signing. This defeats signature confusion: an attacker cannot present a Canon attestation as a different payload type to a generic Ed25519 verifier.
  • Ed25519 is deterministic: the same private key and message always produce the same signature. No random nonce is involved, which eliminates a class of catastrophic implementation failures described in the PS3 case study below.
  • FIPS 186-5 (February 2023) made Ed25519 FIPS-approved. As of 2025 it is the correct default for new evidence systems and Canon's only supported signature algorithm.

Learning objectives

  • Explain what a digital signature proves, and what it does not prove.
  • Describe the DSSE envelope structure and the PAE pre-signing encoding.
  • Describe Ed25519 key generation, signing, and verification at the level required for correct implementation.
  • Distinguish pure Ed25519, Ed25519ph, and hedged Ed25519; choose correctly between them.
  • Identify the algorithm-substitution attack and understand why Canon defeats it by embedding signature_algorithm inside the signed payload. - Implement keygen, sign_dsse, verify_dsse, and PEM export using the cryptography library. --- ## The signing-vs-encryption misconception The phrase "encrypt with the private key" is technically wrong and practically harmful. Code that tries to "encrypt with Ed25519" discovers that the cryptography library does not support it — Ed25519 has no encryption mode — then falls back to something that does, and by that detour introduces a vulnerability. Signing and encryption are distinct operations with different security models, different failure modes, and different API surfaces. The correct description: a signature is computed by the holder of a private key over a message; the signature is verified by anyone who has the corresponding public key. Nothing is encrypted. The message is visible in plaintext. The signature proves that the message has not been altered since the private-key holder signed it, and that no one without that key could have produced that signature. A related misconception: signing proves identity. It does not. It proves key possession. The link between a key and an identity is established by a separate mechanism — in Canon, the public_key_url and public_key_fingerprint fields in the Seal block, which allow a recipient to retrieve the issuer's published key and verify that it matches what was used to sign. The issuance of that key by a credible authority (or by the issuer's own published record) is outside the cryptographic protocol. --- ## The wax-seal metaphor The vocabulary of Ed25519 is unfamiliar to most receivers. This metaphor maps to the actual cryptographic properties without distortion. Consider a signet ring. The ring engraves a unique pattern — your family crest, a personal cipher. You press the ring into hot wax on a sealed letter and the wax hardens. Anyone who knows what your ring looks like (the public shape) can inspect the impression (the signature) and confirm: yes, that is the author's seal, and the letter has not been opened since it was sealed. They cannot reproduce the impression without possessing the ring itself. Now substitute: | Metaphor | Ed25519 | |---|---| | The signet ring | The private key (32 bytes) | | The wax impression | The signature (64 bytes) | | The known ring shape | The public key (32 bytes) | | The letter contents | The chain_hash string | | "Has not been opened" | The signature verifies | | "Could not be forged" | No one without the private key can produce a valid signature | The metaphor breaks in one place: a wax seal can be melted off and re-applied with a counterfeit ring that looks similar. Ed25519 signatures cannot be forged without the private key, not even approximately, because the verification is mathematical rather than visual. --- ## DSSE: the signing envelope (v0.2.0) Canon v0.2.0 adopts DSSE (Dead Simple Signing Envelope), the CNCF in-toto/SLSA standard, rather than signing raw bytes directly. The motivation is signature confusion: a bare Ed25519 signature over 64 bytes of canonical_bytes is structurally identical to an Ed25519 signature over any other 64-byte payload. A verifier that accepts generic Ed25519 signatures cannot tell whether a given signature was produced for a Canon attestation or for an entirely different purpose.

DSSE solves this by encoding the content type and the payload length into the message before signing, using a Pre-Authentication Encoding (PAE):

PAE(payload_type, payload) =
    "DSSEv1\n"
    + len_le8(payload_type) + "\n" + payload_type + "\n"
    + len_le8(payload) + "\n" + payload

where len_le8(s) = 8-byte little-endian encoding of len(s)
%%| label: fig-pae-layout
%%| fig-cap: "DSSE PAE byte layout — what Ed25519 actually signs"
block-beta
  columns 1
  A["'DSSEv1\\n'  (7 bytes — domain separator)"]
  B["len_le8(payload_type)  (8 bytes, little-endian uint64)"]
  C["'\\n'"]
  D["payload_type string  e.g. 'application/vnd.nora.canon.attestation+json; version=0.2.0'"]
  E["'\\n'"]
  F["len_le8(payload)  (8 bytes, little-endian uint64)"]
  G["'\\n'"]
  H["payload bytes  (JCS-canonicalized attestation JSON)"]

Canon's payload type string is "application/vnd.nora.canon.attestation+json; version=0.2.0". A verifier that expects a Canon attestation but receives a different payload type will reject it at the PAE check, before any signature math is attempted.

DSSEEnvelope schema

class DSSESignature(BaseModel):
    keyid: str           # fingerprint of the signing key
    sig: str             # base64url-encoded Ed25519 sig over PAE(payload_type, canonical_bytes)
    public_key_url: str  # stable URL for the public PEM (RFC 8410)

class DSSEEnvelope(BaseModel):
    payload_type: str = "application/vnd.nora.canon.attestation+json; version=0.2.0"
    payload: str              # base64url-encoded JCS(attestation) bytes
    signatures: list[DSSESignature]
    chain_hash: str           # "sha256:<hex>" — SHA-256 of decoded payload bytes (convenience field)

The payload field carries the base64url encoding of canonical_bytes (the JCS output from Chapter 7). The chain_hash field carries SHA-256(canonical_bytes) in sha256:<hex> format. This is a convenience field — a quick-check tool can validate field integrity with just a SHA-256 call, without implementing PAE. Both checks are required for a conformant verifier.

Signing with DSSE

from meridian.canon.signing import sign_dsse, CANON_PAYLOAD_TYPE

sig = sign_dsse(private_key, canonical_bytes)
# Equivalent to: Ed25519.sign(PAE(CANON_PAYLOAD_TYPE, canonical_bytes))

Internally, sign_dsse constructs the PAE bytes and calls private_key.sign(pae_bytes). The PAE construction is deterministic; the same canonical_bytes always produces the same PAE input and therefore the same signature. ### Verification: three required steps A conformant verifier must perform all three steps: 1. Decode payload: base64url-decode dsse_envelope.payloadcanonical_bytes. 2. Check chain_hash: SHA-256(canonical_bytes) must equal the chain_hash field. This detects any tampering with individual attestation fields. 3. Verify signature: Ed25519.verify(sig, PAE(payload_type, canonical_bytes), public_key) against the public key at public_key_url. Step 2 is redundant with step 3 when the signer is honest — if the signature verifies, the payload is intact. But chain_hash allows a lightweight tool to confirm field integrity without implementing PAE, so both checks are required for conformance. > ◆ Going Deeper — Why DSSE rather than JWS or COSE? > > JWS (RFC 7515) puts the payload in a base64url field alongside the signature but does not commit the signing key to a specific payload type in the signed bytes — the alg header is part of the JWT but is not always included in the signing input depending on the library. DSSE is simpler: the entire PAE is what gets signed, and the PAE encodes both type and length. COSE achieves the same goal in binary (CBOR) form. Canon's choice of DSSE follows the in-toto and SLSA ecosystems, which chose it for the same "no envelope surprises" property. Interoperability with supply-chain security tooling is a secondary benefit. --- ## Ed25519 in practice Ed25519 is defined in RFC 8032 §5.1. The key numbers: - Private key: 32 bytes, generated from a cryptographically secure random number generator. In practice, the private key is a seed from which both the actual signing scalar and a nonce-derivation key are derived. - Public key: 32 bytes, derived deterministically from the private key via scalar multiplication on Curve25519. (Curve25519 (the Montgomery-form curve) and Edwards25519 (the twisted Edwards form) are the same mathematical object in different coordinate systems. Ed25519 uses Edwards25519 internally; the external name "Curve25519" refers to the underlying elliptic curve defined by Bernstein et al. in 2006.) - Signature: 64 bytes. Two 32-byte values: a nonce commitment point R and a scalar S. The nonce is derived deterministically from the private key and the message — this is what makes Ed25519 deterministic. - Verification: Given the message, the public key, and the signature, verification is a single curve equation check. It either passes or fails. The cryptography library exposes this as a clean three-function API. Canon's meridian/canon/signing.py wraps it with DSSE envelope support:

# meridian/canon/signing.py — key generation, DSSE sign, DSSE verify
# >>> ch6 start

CANON_PAYLOAD_TYPE = "application/vnd.nora.canon.attestation+json; version=0.2.0"


def _pae(payload_type: str, payload: bytes) -> bytes:
    """Pre-Authentication Encoding (DSSE spec §2.3)."""
    def len_le8(s: bytes) -> bytes:
        return len(s).to_bytes(8, byteorder="little")
    pt = payload_type.encode("utf-8")
    return (
        b"DSSEv1\n"
        + len_le8(pt) + b"\n" + pt + b"\n"
        + len_le8(payload) + b"\n" + payload
    )


def generate_keypair() -> tuple[Ed25519PrivateKey, Ed25519PublicKey]:
    """Generate a fresh Ed25519 keypair."""
    private_key = Ed25519PrivateKey.generate()
    public_key = private_key.public_key()
    return private_key, public_key


def sign_dsse(private_key: Ed25519PrivateKey, canonical_bytes: bytes) -> str:
    """Sign canonical_bytes under DSSE; return base64url-encoded signature.

    Signs PAE(CANON_PAYLOAD_TYPE, canonical_bytes), not the raw bytes.
    """
    pae_bytes = _pae(CANON_PAYLOAD_TYPE, canonical_bytes)
    signature_bytes = private_key.sign(pae_bytes)
    return base64.urlsafe_b64encode(signature_bytes).decode("ascii")


def verify_dsse(
    public_key: Ed25519PublicKey,
    canonical_bytes: bytes,
    signature_b64url: str,
    payload_type: str = CANON_PAYLOAD_TYPE,
) -> bool:
    """Verify a DSSE signature over canonical_bytes."""
    try:
        signature_bytes = base64.urlsafe_b64decode(
            signature_b64url.encode("ascii") + b"=="  # re-pad
        )
    except Exception:
        return False
    pae_bytes = _pae(payload_type, canonical_bytes)
    try:
        public_key.verify(signature_bytes, pae_bytes)
        return True
    except InvalidSignature:
        return False

# <<< ch6 end

Notice what sign_dsse signs: not canonical_bytes directly, but PAE(CANON_PAYLOAD_TYPE, canonical_bytes). The PAE prefix "DSSEv1\n" plus the length-encoded type string binds the signature to Canon's specific payload type. A verifier receiving a DSSE envelope for a different payload type will construct different PAE bytes and the signature will fail. This is an intentional design choice, explained below. --- ## Why DSSE signs the payload, not the chain hash In v0.1.x Canon signed the UTF-8 bytes of the chain_hash string. In v0.2.0, the signature is over PAE(CANON_PAYLOAD_TYPE, canonical_bytes) — the full canonical representation of the attestation, not a hash of a hash. This matters for two reasons. First, the chain hash is a 71-byte string (sha256: + 64 hex chars). Signing it directly would mean that any 71 bytes encoding the same digest — say, a SHA-3-256 digest that happens to be the same hex string — could be confused with a Canon attestation's signature. PAE makes the payload type explicit in the signed bytes. Second, the DSSE envelope's payload field carries the actual canonical bytes (base64url-encoded), so a verifier can decode the payload, verify the signature over it, and inspect the attestation content without a separate storage layer. The chain_hash field in the DSSEEnvelope is retained as a convenience for quick integrity checks — a verifier can confirm SHA-256(decoded_payload) == chain_hash without implementing PAE. Both checks are required; neither is optional.


PEM export (RFC 8410)

Canon publishes the public key at a stable URL. The format is PEM, per RFC 8410. The PEM wraps an ASN.1 SubjectPublicKeyInfo structure that encodes the Ed25519 OID (1.3.101.112) and the 32-byte key material.

# meridian/canon/signing.py — PEM round-trip

def public_key_to_pem(public_key: Ed25519PublicKey) -> bytes:
    """Serialize public key to RFC 8410 PEM (PKIX SubjectPublicKeyInfo)."""
    return public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )

def public_key_from_pem(pem_bytes: bytes) -> Ed25519PublicKey:
    """Parse PEM-encoded Ed25519 public key."""
    key = serialization.load_pem_public_key(pem_bytes)
    if not isinstance(key, Ed25519PublicKey):
        raise ValueError(f"Expected Ed25519PublicKey, got {type(key).__name__}")
    return key

A verifier that receives an attestation fetches the URL in seal.public_key_url, parses the PEM, and calls public_key.verify(...). The SHA-256 fingerprint of the PEM bytes is checked against seal.public_key_fingerprint before verification. This prevents an attacker from substituting a different public key at the URL — the fingerprint is inside the signed payload and cannot be altered without invalidating the signature. --- ## Pure Ed25519 vs Ed25519ph vs hedged RFC 8032 defines two variants of EdDSA for Curve25519: - Ed25519 (pure): The entire message is hashed internally during signing. The message can be any length. - Ed25519ph (prehash): The caller hashes the message before passing it to the signing function. Designed for streaming large messages where you cannot hold the whole message in memory simultaneously. Canon uses pure Ed25519. The rationale is simple: the PAE bytes Canon signs are on the order of a few kilobytes at most (the CANON_PAYLOAD_TYPE string plus the canonical attestation bytes, all small relative to the message-size threshold where Ed25519ph makes sense). There is no memory pressure. Ed25519ph adds complexity (and a different domain separator that makes signatures incompatible with pure Ed25519) without any benefit at this message size. The third option — hedged Ed25519 — is more interesting and more contested. Pure Ed25519 derives its nonce deterministically: per RFC 8032 §5.1, the private key seed is first expanded via SHA-512(private_seed) → 64 bytes, and the nonce is then computed as SHA-512(expanded_key[32:64] || message), where the second 32 bytes of the expanded key serve as a nonce-derivation key. The hash function throughout is SHA-512, not SHAKE256. This is the property that makes Ed25519 immune to the PS3 disaster (below). The downside is that if the deterministic nonce function ever produces the same nonce for two different messages — which cannot happen with a correct implementation but can happen with a faulty hardware RNG or a differential fault attack — the private key is recoverable from the two signatures algebraically. Hedged Ed25519 (draft-irtf-cfrg-det-sigs-with-noise) addresses this by mixing a random value into the nonce derivation: nonce = SHA-512(expanded_key[32:64] || random_bytes || message). The hash function remains SHA-512; the nonce is still deterministic given the expanded key and the random bytes, so replay attacks (sign the same message twice, get the same signature) are still detected — but the fault-attack surface is smaller. Canon does not implement hedged Ed25519. For software implementations running on general-purpose hardware, pure Ed25519 is correct and the fault-attack risk is negligible. For evidence systems deployed on specialized hardware (smart cards, HSMs, embedded devices) where differential fault injection is a realistic threat, the migration to hedged signatures is worth the implementation cost. The Build-your-own prompt at the end of this chapter asks you to evaluate this for your own capstone. > ◆ Going Deeper — What deterministic nonce derivation actually prevents. > > Standard ECDSA requires a random per-signature nonce k — if k is reused or predictable, the private key can be recovered. RFC 6979 eliminates this risk by deriving k deterministically from the private key and message via HMAC-DRBG, making the nonce reproducible but unpredictable to any party without the private key. Ed25519 takes the same approach but uses SHA-512 directly. > > The recovery algebra: given two signatures (r, s1) and (r, s2) on messages m1 and m2 with the same k (same r), the attacker can compute k = (H(m1) - H(m2)) / (s1 - s2) mod n. From k, the private key follows directly. (Derivation: from s = k^-1 * (H(m) + r*d) mod n, with the same k, s1 - s2 = k^-1 * (H(m1) - H(m2)) mod n, therefore k = (H(m1) - H(m2)) / (s1 - s2) mod n.) This is not a theoretical attack. > > Ed25519 makes the nonce a deterministic function of the private seed and the message. No RNG is involved in signing. A correct Ed25519 implementation cannot repeat a nonce across two different messages, because the nonce derivation function is injective. The PS3 attack (below) was an ECDSA failure; Ed25519 is structurally immune. --- ## The algorithm-substitution attack Suppose an attacker intercepts a Canon attestation and replaces the signature with a forged signature produced under a different algorithm — say, a weak RSA signature that the attacker can forge. If the signature algorithm is not committed inside the signed payload, the verifier cannot distinguish a valid Ed25519 verification from a valid RSA verification: it just calls verify(public_key, message, signature) and receives True or False. In v0.2.0, the DSSE envelope provides two layers of defense against this attack: Layer 1 — payload_type is verified before the signature. The DSSEEnvelope.payload_type field is "application/vnd.nora.canon.attestation+json; version=0.2.0". A conformant verifier checks this string as a precondition before constructing the PAE and calling Ed25519.verify. An attacker who changes payload_type to something else changes the PAE bytes — and the signature fails. An attacker who leaves payload_type correct but substitutes a different signature algorithm faces the second layer. Layer 2 — DSSESignature.keyid must match the public key type. The verifier fetches public_key_url, parses the PEM, and confirms it is an Ed25519 key (OID 1.3.101.112 per RFC 8410). If the fetched key is RSA, the verifier rejects before attempting to verify. The protection is behavioral but explicit: the verifier must check the key type before calling verify. The relevant schema fields in meridian/canon/schema.py:

class DSSESignature(BaseModel):
    keyid: str           # SHA-256 fingerprint — checked against fetched PEM before verify
    sig: str             # base64url Ed25519 sig over PAE(payload_type, canonical_bytes)
    public_key_url: str  # fetched; must be Ed25519 key (RFC 8410 OID 1.3.101.112)

class DSSEEnvelope(BaseModel):
    payload_type: str    # checked as precondition — must equal CANON_PAYLOAD_TYPE
    payload: str         # base64url canonical bytes
    signatures: list[DSSESignature]
    chain_hash: str      # sha256:<hex> — convenience check

These checks must be implemented explicitly in the verifier. The cryptographic Ed25519.verify call does not enforce them. --- ## FIPS 186-5 The National Institute of Standards and Technology published FIPS 186-5 in February 2023. It is the first FIPS publication to approve Ed25519. Previously, Ed25519 was considered "FIPS-unapproved" for federal use, which blocked its adoption in government systems and in commercial systems that require FIPS compliance for regulatory reasons. FIPS 186-5 approval matters for Canon in three ways. It removes the last institutional objection to Ed25519 in environments that require FIPS compliance — hospitals, financial institutions, federal contractors. It signals that the algorithm has survived the scrutiny NIST applies to signature standards, the same body that approved RSA in 1994 and ECDSA in 2000. And it creates a stable regulatory foundation: the algorithm will not be deprecated without a multi-year transition period. The approval covers pure Ed25519 as defined in RFC 8032. Ed25519ph and hedged variants are addressed separately and with different status. Canon's use of pure Ed25519 is the FIPS-approved variant. --- > § For the Record — RFC 8032 §5.1, the signing algorithm. > > "The inputs to the signing procedure is the private key, a 32-octet string, and a message M of arbitrary length. For Ed25519, dom2(x,y) is the empty string, phflag is 0 [...] > > 1. Hash the private key, 32 octets, using SHA-512. Let h denote the resulting digest. Construct the secret scalar s from the first half of the digest [...]. > 2. Compute SHA-512(dom2(F, C) || prefix || PH(M)), where M is the message to be signed. Call the digest r. > 3. Compute the point [r]B. For efficiency, do this by first reducing r modulo l, the group order of B. Let R = [r]B. > 4. Compute SHA-512(dom2(F, C) || R || A || PH(M)), and interpret the 64-octet digest as a little-endian integer k. > 5. Compute S = (r + k * s) mod l." > > RFC 8032 §5.1 (January 2017). The phflag = 0 in step 1 distinguishes pure Ed25519 from Ed25519ph. --- > ☉ In the Wild — The PlayStation 3. > > In December 2010, a group of researchers known as fail0verflow presented a complete break of Sony's PlayStation 3 content-signing system at the 27th Chaos Communication Congress. The system used ECDSA. The private key was not leaked; no cryptographic primitive was broken. Sony had signed every piece of PS3 software with the same nonce k. > > When the nonce is constant, the recovery formula produces the private key from any two signatures on any two messages. The researchers needed only to observe two signed binaries — trivially obtained from retail PS3 software — and the algebra followed. > > The consequence: every PS3 ever manufactured accepted arbitrary code signed with the recovered private key. Sony issued firmware updates that could not un-ring the bell; the root key was permanently compromised. > > Ed25519 is immune to this class of failure. The nonce is derived as a function of the private seed and the message. There is no random number generator involved in Ed25519 signing. A correct implementation cannot use the same nonce twice on different messages. > > The PS3 case is not a cautionary tale about weak keys or weak ciphers. It is a cautionary tale about nonce generation. Ed25519's deterministic nonce is the architectural fix, written into the algorithm specification rather than left to the implementer. --- > ▼ Why It Matters — The parent's attorney and the expert's laptop. > > In the 2026 TPR proceeding, the parent's advocate has published an Ed25519 public key on a stable URL. The Seal block of every attestation includes the fingerprint of that key and the URL where it can be fetched. The opposing party's expert brings a laptop to the courtroom. > > The expert fetches the public key from public_key_url, verifies the fingerprint, base64url-decodes the DSSE envelope's payload to get canonical_bytes, confirms SHA-256(canonical_bytes) == chain_hash, and calls verify_dsse on the decoded payload. The verification either passes or fails. At no point does the expert need access to the advocate's computer, the advocate's private key, the database that produced the attestation, or any cloud service. The verification is complete on the expert's own machine with the attestation JSON and the public key PEM.

This is the falsifiability promise. The signature is what makes it executable: the claim is not "trust us, the chain hash was intact when we sealed this" — it is "verify it yourself, here is the key, here is the algorithm, here is the signed hash."

A hash alone cannot make this promise. A hash proves the bytes are intact; it does not prove who committed to those bytes or that the hash was computed before the litigation began.


Try This — Sign with DSSE, verify, then tamper.

Open a Python shell. Generate a keypair. Produce some canonical bytes, sign them with DSSE, and verify.

import base64
from meridian.canon.signing import generate_keypair, sign_dsse, verify_dsse
priv, pub = generate_keypair()
canonical_bytes = b'{"attestation_id":"test"}'  # stand-in for real JCS output
sig = sign_dsse(priv, canonical_bytes)
print(verify_dsse(pub, canonical_bytes, sig))   # True

Now flip one character of the base64url signature. Call verify_dsse again. You will see False. > > The point: Ed25519 verification is not fuzzy. It does not have "almost valid" or "96% match." The math either holds or it does not. A single bit of alteration in the 64-byte signature causes verification to fail. > > Now try calling verify_dsse with the correct signature but a different canonical_bytes. You will also see False — the PAE is computed over the payload, so any change to the payload invalidates the signature even if the signature bytes themselves are intact. --- ## Working example The full implementation lives in meridian/canon/signing.py. The functions you need for Lab 6 are generate_keypair, sign_dsse, verify_dsse, and public_key_to_pem. Key storage (macOS Keychain integration) is in meridian/canon/keys.py. The DSSEEnvelope and DSSESignature Pydantic models live in meridian/canon/schema.py (Chapter 8). The test suite at meridian/canon/tests/test_emit_walk.py covers the DSSE signing round-trip (keygen → sign_dsse → verify_dsse → True), tamper detection (flip a signature byte → verify_dsse → False), and PEM serialization (key → PEM → key → sign_dsse → verify_dsse), exercised through the full emit→walk protocol. For Lab 6, you will walk a minimal attestation through meridian.canon.walk and confirm that step 2 (DSSE signature verification, including chain_hash cross-check) passes. The walk function calls verify_dsse internally; you are connecting the two ends of the chain you built in this chapter. --- ## Lab 6 Goal: Confirm that keygen, sign_dsse, verify_dsse, and the reference walker all agree on a minimal Canon v0.2.0 attestation. Deliverables: 1. Use generate_keypair to create a keypair. Export the public key to PEM. Compute the SHA-256 fingerprint of the PEM bytes. (This fingerprint is what would go in seal.public_key_fingerprint.) 2. Using the test fixtures in meridian/canon/tests/, find or construct a minimal Attestation JSON. Canonicalize it with meridian.canon.canonicalizecanonical_bytes. Compute chain_hash = "sha256:" + sha256_hex(canonical_bytes). Call sign_dsse(private_key, canonical_bytes) to get the base64url signature. Construct a DSSEEnvelope with payload=base64url(canonical_bytes), the signature, and the chain_hash field. 3. Run meridian-canon walk <your_attestation.json>. Confirm the verdict is valid. Confirm step 2 (step2_signature_verify) shows pass. 4. Deliberately alter one byte of the base64url signature in the envelope. Re-run the walker. Confirm the verdict is invalid and step 2 shows fail. 5. Deliberately alter the chain_hash field in the envelope (leave the payload and signature intact). Re-run the walker. Confirm the verdict is invalid and step 2 shows fail (SHA-256 of decoded payload no longer matches the declared chain_hash). Acceptance criteria: All five steps complete, both failure modes are confirmed, and the lab directory contains your attestation JSON and a short findings.md documenting each result. ---

💡Key Takeaways
- Ed25519 signs the PAE bytes — "DSSEv1\n" + len(payload_type) + payload_type + len(canonical_bytes) + canonical_bytes — not the raw content, which means substituting the payload type or the canonicalized bytes invalidates the signature even if the signature bytes themselves are unchanged. - DSSE prevents signature confusion attacks because the payload_type string "application/vnd.nora.canon.attestation+json; version=0.2.0" is encoded in the PAE before signing, so a verifier that receives a different payload type constructs different PAE bytes and the signature fails. - The three-step verification protocol is: (1) base64url-decode payloadcanonical_bytes, (2) confirm SHA-256(canonical_bytes) == chain_hash, (3) verify Ed25519.verify(sig, PAE(payload_type, canonical_bytes), public_key) — all three are required; step 2 alone does not authenticate the issuer. - public_key_url enables anyone to verify without the custodian because the public key is at a stable HTTPS URL committed inside the signed payload, and the URL's fingerprint is checked against keyid before verification so a substituted key is caught. - The payload_type string is itself a falsifiable claim — it encodes the Canon version, so an attestation signed under v0.2.0 cannot be presented to a v0.2.0 verifier as if it were a different format without the signature failing.
## Exercises ### Warm-up 1. Read Soatok's "A Furry's Guide to Digital Signature Algorithms" (linked in Further Reading). In 100 words, explain why Ed25519 was selected over ECDSA-P256 for Canon. Focus specifically on the nonce-generation difference. 2. Open RFC 8032 §5.1 and locate the step that derives the nonce r. Identify which inputs are used. Confirm that no external randomness is called. Write one sentence explaining what this means for replay attacks (signing the same message twice). ### Core 3. Implement an algorithm-substitution attack in miniature: write a signing protocol that stores the signature alongside the algorithm name, but in a separate field that is not included in the signed message. Show that an attacker can replace the algorithm name without invalidating the signature. Then patch the protocol: include the algorithm name in the signed message and confirm the attack fails. 4. Canon's verify function returns False rather than raising an exception on invalid signatures. Write a test that confirms this behavior for four distinct failure modes: (a) wrong public key, (b) corrupted signature bytes, (c) altered message, (d) malformed base64. For each, confirm verify returns False and does not raise. 5. Read meridian/canon/signing.py. The verify_dsse() function catches two different exception types in two separate try/except blocks. Write one sentence explaining what each block catches and what it means about the attestation if each exception is raised. ### Stretch 5. Read the Romailler and Pelissier FDTC 2017 paper on EdDSA fault attacks against embedded implementations (linked in Further Reading). In two paragraphs, outline what would have to change in meridian/canon/signing.py to support hedged Ed25519 once the CFRG draft finalizes. Specifically: what new input is required, how does the nonce derivation change, and are signatures produced by the hedged variant compatible with the pure-Ed25519 verifier? 6. The reference walker (Chapter 25) calls verify_dsse(public_key, canonical_bytes, sig). Construct a test where the canonical bytes are the correct attestation bytes but the DSSE envelope's payload_type is changed from CANON_PAYLOAD_TYPE to "application/json". Confirm that verification fails — the PAE input changes when the type changes. This is a concrete demonstration of why the payload type is load-bearing in DSSE. --- ## Build-your-own prompt For your capstone system, identify the threat model that determines which Ed25519 variant is appropriate. Answer three questions: (1) Is the signing hardware software-only (general-purpose CPU) or specialized hardware (HSM, smart card, embedded microcontroller)? (2) Is the signing key stored in a hardware security module, in encrypted storage, or in plaintext on disk? (3) Is differential fault injection a realistic threat given the physical security of the signing environment? Based on your answers, justify in two paragraphs whether pure Ed25519 (Canon's current choice), Ed25519ph (large-message streaming), or hedged Ed25519 (fault-tolerant) is the right choice for your system. --- ## Further reading - DSSE specification (CNCF in-toto), https://github.com/secure-systems-lab/dsse/blob/master/spec.md. The envelope format Canon v0.2.0 adopts. §2.3 defines PAE. - SLSA provenance format, https://slsa.dev/provenance. The DSSE use in supply-chain security that motivates Canon's adoption. - RFC 8032 — Edwards-Curve Digital Signature Algorithm (EdDSA), https://datatracker.ietf.org/doc/html/rfc8032. The primary specification for Ed25519. §5.1 is the signing algorithm. - RFC 8410 — Algorithm Identifiers for Ed25519, Ed448, X25519, and X448, https://datatracker.ietf.org/doc/html/rfc8410. PEM encoding for Ed25519 keys. - FIPS 186-5 (February 2023) — Digital Signature Standard, https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf. The NIST approval that brought Ed25519 into regulated environments. - Soatok, "A Furry's Guide to Digital Signature Algorithms," https://soatok.blog/2020/04/26/a-furrys-guide-to-digital-signature-algorithms/. Accessible technical comparison of RSA, ECDSA, and EdDSA. - Romailler and Pelissier, "Practical Fault Attack against the Ed25519 and EdDSA Signature Schemes," FDTC 2017, https://romailler.ch/project/eddsa-fault/. The hardware fault attack that motivated the hedged variant. - CFRG draft, draft-irtf-cfrg-det-sigs-with-noise — the hedged Ed25519 proposal. Track at https://datatracker.ietf.org/doc/draft-irtf-cfrg-det-sigs-with-noise/. - fail0verflow, "PS3 Epic Fail," 27C3 (2010), https://media.ccc.de/v/27c3-4087-en-console_hacking_2010. The PS3 ECDSA presentation; the algebra starts around minute 48. - research/01_cryptography_pedagogy.md — the dossier entry on this chapter's pedagogical choices.


Previous: Chapter 5 — Chain Hashing. Next: Chapter 7 — Canonicalization (RFC 8785).