Identity, pairing and revocation
1. Root identity
Section titled “1. Root identity”A node’s root identity is an Ed25519 keypair, IK.
IK_priv MUST be generated inside a platform secure element — Secure Enclave,
Android Keystore with StrongBox, TPM 2.0, or equivalent — where the platform provides
one, and all signing MUST be delegated to that element.
Software key custody is permitted only in explicitly flagged development builds. An implementation MUST fail closed rather than silently downgrade, and MUST surface the custody mode to the user. Silent downgrade is the failure that matters here: a user who believes their key is hardware-protected and is wrong has strictly worse security than one who knows it is not.
Only three things ever leave the node: IK_pub (32 bytes), signatures made by the
element, and the ephemeral public key used during pairing. IK_priv never does, and
the interface deliberately exposes no export operation.
site_id and mesh_id derive from IK_pub as defined in
terminology.
2. Pairing
Section titled “2. Pairing”Pairing establishes mutual trust between two nodes. It does not distribute a shared secret, because SSP/1 has no shared-key mechanism: events are signed rather than sealed, and transport confidentiality belongs to the binding. The output of a successful pairing is an entry in each side’s trusted-peer keyring and nothing else.
2.1. Key agreement
Section titled “2.1. Key agreement”Enroller E and candidate C exchange offers over any untrusted transport. The
offer’s wire encoding is a deterministic CBOR map:
PairingOffer = { ik_pub : bstr 32 Ed25519 root identity public key ek_pub : bstr 32 X25519 ephemeral public key nonce : bstr 16 random site_id : bstr 16}Every field is REQUIRED; a receiver MUST reject an offer with missing, extra or
mis-sized fields. site_id MUST bind to ik_pub — a receiver recomputes it and
rejects a mismatched offer.
The offer is not itself a COSE message. It is a transient ceremony artifact, authenticated by the transcript signature below and by the out-of-band comparison, and it is never stored or forwarded — the dividing rule in the registry.
Both sides compute the transcript, with sort meaning lexicographic ordering on raw
bytes so that both derive the same value regardless of role:
T = "sovm-mesh-pair-v1" || sort(ik_pub_E, ik_pub_C) || sort(ek_pub_E, ek_pub_C) || sort(nonce_E, nonce_C)then the X25519 shared secret dh, and the session key:
SK = HKDF-SHA256(ikm = dh, salt = T, info = "pair-session", L = 32)Each side signs T with IK_priv inside its secure element and verifies the peer’s
signature against the offered ik_pub. Failure aborts the ceremony with no
downgrade path.
Binding the transcript into the salt is what makes the session key specific to this
exchange: an attacker replaying a recorded offer cannot arrive at the same SK
without also controlling both nonces and both ephemeral keys.
SK has exactly one purpose: deriving the SAS below. A predecessor also used it to
wrap a shared fleet key during pairing; SSP/1 has no shared key to transfer, so
nothing else is derived from it. On ceremony completion or abort, SK and both
ephemeral private keys MUST be zeroized.
2.2. Out-of-band authentication
Section titled “2.2. Out-of-band authentication”Exactly one authenticator is REQUIRED. The key agreement above authenticates the keys to each other; only a human comparison authenticates them to the right peer, which is what defeats a machine-in-the-middle.
QR commitment. BLAKE3(ik_pub || ek_pub || nonce), 32 bytes, displayed by one
side, scanned by the other, and compared in constant time against the offer received
over the transport.
Short authentication string (SAS). Derived from the session key and compared aloud or on screen:
okm = HKDF-SHA256(ikm = SK, salt = "", info = "pair-sas", L = 8)digits = int_be(okm) mod 10^12sas = digits, zero-padded to 12 characters, grouped 4-4-4int_be is big-endian interpretation of the 8 octets as an unsigned integer.
Two details are load-bearing and easy to get wrong:
- The reduction is explicit. A predecessor specified “40 bits, rendered as 12 zero-padded decimal digits” — which is impossible, since 2⁴⁰ exceeds 10¹² and 12 digits cannot represent the range. Implementations silently applied a modular reduction the specification never mentioned. Because both sides must display identical digits or the ceremony aborts, an unstated reduction is an interoperability failure, not a formatting detail.
- The wide input keeps it near-uniform. Reducing 64 bits into 10¹² leaves a modulo bias below 2⁻²⁰ of a digit’s worth, which is negligible. Reducing exactly 40 bits would have produced a measurable bias toward low values — the reason the derivation is 8 octets rather than 5.
The resulting strength is log₂(10¹²) ≈ 39.9 bits against a one-shot machine-in-the-middle, which must be committed to a guess before the humans compare.
An implementation MUST accept only a human match/mismatch verdict. It MUST NOT expose any interface that ingests a peer-transmitted SAS value, because a SAS compared by software over the same channel it is meant to authenticate proves nothing.
2.3. Ceremony state machine
Section titled “2.3. Ceremony state machine”Both sides run the same machine. Any failure lands in aborted; there are no other
exits and no retry-in-place.
| State | Transition on | Next state |
|---|---|---|
idle |
Offer sent and peer offer received | offers_exchanged |
offers_exchanged |
site_id binding and both transcript signatures verify |
transcript_verified |
offers_exchanged |
Any verification failure | aborted |
transcript_verified |
Human confirms the QR or SAS comparison | authenticated |
transcript_verified |
Human reports mismatch, or timeout | aborted |
authenticated |
Peer appended to the trusted-peer keyring | paired |
Rules:
- A ceremony MUST NOT be resumed after
aborted. A retry is a new ceremony with fresh ephemeral keys and nonces — reusing either would let a machine-in-the-middle retry against the same comparison values. - An implementation MUST NOT surface the out-of-band comparison before reaching
transcript_verified, or the human is comparing digits for keys nobody has proven possession of. - Zeroization of
SKand the ephemeral keys happens on every path out of the machine,pairedincluded.
2.4. Persistence
Section titled “2.4. Persistence”Each side appends the peer to its trusted-peer keyring:
| Field | Type | Notes |
|---|---|---|
site_id |
16 bytes | Primary key |
ik_pub |
32 bytes | Peer’s Ed25519 root identity public key |
paired_at |
timestamp | |
method |
enum | qr | sas | vouch |
vouched_by |
string | "self" for direct pairing, else the voucher’s site_id |
site_id MUST always be re-derived from the stored ik_pub rather than trusted as
transmitted.
Re-adding the same pair is idempotent. Presenting a different ik_pub for an
existing site_id MUST hard-fail as an identity conflict. An implementation MUST NOT
overwrite: recovery is an explicit, human-initiated re-pair. Silent overwrite here is
indistinguishable from a successful impersonation.
3. Vouching
Section titled “3. Vouching”An already-trusted node may vouch a new node into the mesh, so that joining does not require a proximity ceremony with every existing member.
3.1. Vouch certificate
Section titled “3.1. Vouch certificate”A vouch is a COSE_Sign1 record under
the SSP/1 COSE profile:
Vouch = COSE_Sign1( protected = { alg : EdDSA, content type : "sovm/ssp-vouch-v1", kid : voucher site_id (16 bytes) }, payload = deterministic CBOR { subject_ik_pub : bstr 32, subject_site_id : bstr 16, issued_at_s : unsigned seconds }, external_aad = mesh_id (16 raw bytes))The voucher’s identity travels in the protected kid; a verifier resolves the
signing key from its own keyring, which is what “locally trusted voucher” means
mechanically. The mesh binds through external_aad, so a vouch is
non-transferable across meshes by construction — in any other mesh the
signature simply does not verify.
A predecessor encoded vouches as a hand-assembled pre-image with length-prefixed fields to keep the encoding injective. Deterministic CBOR maps are injective by construction, so that discipline — and the class of bugs it guarded against — is gone.
Validity is 86 400 s from issued_at_s, with 300 s future-dating tolerance. The
window is short on purpose — a vouch only has to survive long enough to seat the
joining node, after which the subject’s ik_pub lives in the keyring and its
events verify against the keyring rather than against the vouch.
Vouches travel in the sync exchange’s
records member or through the mailbox. They are
self-verifying, so the channel needs no additional trust.
3.2. Verification order
Section titled “3.2. Verification order”A verifier MUST check, in this order:
- the record decodes under the COSE profile — content type known, unprotected bucket empty, payload fields present and correctly sized;
subject_site_idbinds tosubject_ik_pub;- the protected
kidresolves in the local keyring — the voucher is trusted, and trusted for exactly the key the keyring holds; - the validity window, including future-dating tolerance;
- the COSE signature, with
external_aadset to the localmesh_id.
The order is normative because each step narrows what the next one has to consider, and checking the signature first would mean verifying a cryptographically valid certificate about a key the verifier has no business accepting. Mesh non-transferability needs no separate step: a vouch from another mesh fails step 5.
3.3. Join acceptance
Section titled “3.3. Join acceptance”Acceptance requires both, independently:
- At least k valid vouches from distinct vouchers. k = 2 where the mesh already has three or more nodes, otherwise k = 1. Self-vouches and duplicate vouchers do not count toward the threshold.
- An affirmative human confirmation on the seating node. The prompt MUST name both
fingerprints — the first 4 bytes of each
site_id, upper hex, renderedXXXX-XXXX, as a display aid only. The default MUST be deny.
Requiring both is defense in depth: the quorum defends against one compromised trusted node, and the human confirmation defends against a quorum obtained by compromising several.
4. Revocation
Section titled “4. Revocation”Removing a node is three steps:
- Remove it from the trusted-peer keyring, so its future events fail signature verification at the apply chokepoint.
- Deny it in policy, so senders stop offering it data.
- Where SOP/1 is deployed, execute the corresponding MLS removal so the node does not receive future group secrets.
Step 1 does more than it appears to, because every durable record resolves its signer through the keyring: once the entry is gone, the removed node’s transport bindings stop resolving — so its live connections fail authentication — and any vouches it issued stop verifying. One removal, one place, and everything downstream of the identity fails closed together.
Revocation is future-facing. It stops the node receiving new data; it cannot retract what the node already holds. See limitations.
SSP/1 defines no wire message announcing a revocation. A peer enforces a removal only once it has independently received the policy change, and the protocol does not bound that window. See limitations.