Skip to content

Normative · Part

Signature profile

This page is the normative home of the SOVM COSE profile. Both layers bind to this page; neither restates it. That is deliberate: a restatement is a copy that can drift, and a profile stated twice is two profiles the moment one statement is edited.

The profile adopts [RFC 9052] and [RFC 9053] as normative dependencies.

Every durable signed structure in SOVM/1 is a COSE_Sign1 message under a profile deliberately narrower than COSE allows:

Rule Value
Message types COSE_Sign1; plus COSE_Encrypt0 where a layer defines wrapped-key records
Signature algorithm ES256 only — section 5
Encryption algorithm A256GCM only, and only for COSE_Encrypt0
Payload encoding Deterministic CBOR ([RFC 8949] section 4.2.1)
kid The signer’s 16-byte site_id, in the protected bucket
external_aad Per-record context under the context principle — at minimum the verifier’s own 16-byte mesh_id
Unprotected bucket MUST be empty
Unknown critical headers Fail closed
Unknown content type Fail closed
Algorithm agility None — a second suite is added only for a concrete interoperability need

The synchronization layer defines no encryption records, so it instantiates COSE_Sign1 only; the object plane adds COSE_Encrypt0 for its wrapped-key records and binds richer external_aad context. Those are instantiation choices, stated on the layer pages. The rules in the table are not.

The profile also reaches the shared chained-record schema used by membership authorization records (SOP/1 §5.7) and sequencing receipts (SOP/1 §22.5): every link of either chain is a COSE_Sign1 under this profile.

Unknown critical headers or unsupported required algorithms MUST fail closed.

5. Exactly one signature suite, and it is ES256

Section titled “5. Exactly one signature suite, and it is ES256”

The profile permits exactly one suite: A256GCM for COSE_Encrypt0 and ES256 — ECDSA with P-256 and SHA-256, COSE algorithm -7 — for COSE_Sign1.

ES256 is the only admissible COSE_Sign1 suite; EdDSA (-8) is not admitted alongside it. A second mandatory-to-implement suite is added only when a concrete platform or KMS interoperability requirement exists; unused algorithm agility is treated as attack surface, not as flexibility.

The signature is the RFC 9053 fixed-width r ‖ s encoding, 32 bytes each, 64 bytes total. Verifiers MUST reject a DER-encoded ECDSA signature, and MUST reject any signature whose length is not exactly 64 bytes.

The curve is P-256 and only P-256. An implementation MUST use the domain parameters of [SEC 1] / [NIST SP 800-186] for secp256r1 and MUST NOT accept curve parameters, a crv value, or a named-curve identifier carried in or alongside a message. There is one curve; there is nothing to select; a message that appears to select one is malformed.

One deliberate asymmetry is worth naming here so nobody reads it as an oversight: the transport channel key of the default binding profile remains a 32-byte Ed25519 key. It is not a signing identity under this profile — it is a TLS raw-public-key authenticator ([RFC 7250] shape) whose format is what the transport ecosystem, iroh included, actually carries. The record that binds a channel key to a node identity is, like every durable record, signed under this profile.

A repeated or biased per-signature nonce k leaks an ECDSA private key outright, from as few as two signatures.

The rule is per key, not per node. key_protection (SSP/19 identity) describes custody of a node’s root identity key; it does not describe custody of every key that node signs with, and a nonce rule keyed on it would leave a software key on a hardware-attested node bound by nothing.

Where a signing key is held in a secure element, the nonce is generated inside that element under the certification that element carries.

Where a signing key is not held in a secure element, the signer MUST implement deterministic ECDSA per [RFC 6979]. This reaches every software signer, including:

  • a node whose key_protection is software, for all of its signatures; and
  • an event-signing subkey, on a node at any key_protection level — including hardware_attested.

The second case is the one a per-node trigger got wrong, and it is the case that matters most: an event-signing subkey signs at bulk-import volume, so a biased nonce generator has the largest possible sample to leak from.

For any valid ECDSA signature (r, s) over the group of order n, the pair (r, n − s) is also a valid signature over the same message under the same key.

Signers MUST produce the low-S form, s ≤ (n − 1) / 2. Verifiers MUST reject a signature whose s is not in low-S form. Rejecting is not optional and MUST NOT be normalised away by the verifier, because the message bytes — not the parsed value — are what downstream identifiers hash (section 10).

Before using a claimed public key, an implementation MUST check that both coordinates lie in [0, p − 1], that the point satisfies the curve equation, and that it is not the point at infinity.

P-256 has cofactor 1, so those checks are sufficient. An implementation MUST NOT add a subgroup check copied from a curve that needs one; it is not a harmless extra, it is a divergence in what the verifier accepts.

A P-256 point has more than one standard SEC 1 encoding: compressed, 33 bytes, and uncompressed, 65 bytes. They denote the same point and hash differently.

Every derivation or public-key comparison in SOVM/1 that takes an ES256 public key as input MUST take the 33-byte compressed encoding, and implementations MUST reject any other encoding rather than converting it. This reaches the node root public key and the sequencer’s receipt-signing public key of SOP/1 §22.5, wherever either is presented — in a node identity record, a pairing transcript, a COSE_Key, a kid resolution path, and in the fleet’s registration record for a sequencer’s receipt-signing identity.

The receipt-signing key is named because it is not a node root and would otherwise fall outside this clause, and because the check that makes sequencer equivocation actionable is a public-key comparison over exactly that key: a receipt is refused unless it is signed by the key the fleet currently has registered for that sequencer. With two admissible encodings of one point that comparison has two answers. Pinning the wire encoding alone is not sufficient, because a non-conforming encoding can be written into the registration record itself — implementations that normalise then accept receipts that conforming implementations reject, and the fleet partitions on the precise property that makes singularity enforceable. The record is where the rejection has to bite.

This clause is not a tidiness rule. Because a P-256 point has more than one standard encoding, BLAKE3(IK_pub) is unambiguous only with its input pinned — and the derivations that depend on that are load-bearing identity:

Derivation Normative home Effect if the encoding is not pinned
site_id = BLAKE3(IK_pub) truncated SSP/1 terminology One key yields two node identities
mesh_id SSP/1 terminology One founding key yields two meshes
Pairing transcript commitment SSP/1 identity A commitment does not match a re-derivation over the same key
Sequencer receipt-signing key registration and comparison SOP/1 §22.5 A fleet partition on which receipts verify; equivocation detection and singularity both become implementation-dependent

Rejecting rather than converting matters. A verifier that normalises an uncompressed key to compressed form before hashing agrees with a conforming peer about the identity but disagrees with it about which byte strings are well-formed — so a record one accepts and the other rejects becomes a partition, and a site_id becomes reachable by two distinct presentations.

Note that this clause reaches the public key, which is not covered by section 7 — that clause reaches signature values, and neither reaches the other.

10. Canonicity is an identity property, not a hygiene property

Section titled “10. Canonicity is an identity property, not a hygiene property”

Record identity in SOVM/1 is derived from signed bytes — the one-hash convention. The synchronization layer defines event_id as BLAKE3 over “the exact COSE_Sign1 bytes as received” and uses it as the idempotency key that deduplicates re-delivery. The same construction produces object_manifest_hash, the links of the §5.7 authorization chain, and previous_receipt_hash, the link of the §22.5 sequencing-receipt chain.

The receipt chain is named here explicitly rather than left to inference. Section 4 places sequencing receipts inside the shared chained-record schema, so section 7 and section 9 reach them and the property is satisfied. That chain’s continuity is the evidence gates G4 and G10 are gathered against. It is the last identity in SOVM/1 that should have to be inferred.

So a malleated copy of a genuine record — same key, same payload, valid signature, different bytes — carries a different identifier, survives deduplication, and is applied twice. The clauses of section 7 and section 9 are the reason that cannot happen, and they are mandatory for that reason rather than for tidiness.

This class of defect is not specific to ECDSA. Ed25519 has its own canonicity pitfalls — non-canonical S and R encodings, small-order public keys, cofactored versus cofactorless verification — which is worth knowing wherever an Ed25519 key does appear in the stack: the transport channel key of the default binding profile is one.

storage_id is unaffected: it is the iroh-blobs hash of the encrypted object, not of a signature.