01 — Identity#
An ACE identity is defined by a key event log (KEL): an ordered list of signed events whose first event, the inception, names the identity. The KEL governs control and signing only. Packet encryption keys (prekeys) live in the descriptor (04).
An identity holds:
- A root key (
ml-dsa-65) that signs only KEL events. - A committed next root: the SHA-256 digest of the key that will replace the root. Control is anchored in this commitment, not in a signature algorithm. Optionally, a committed fallback root: the digest of a second key that may replace the root instead, ranked below the next root (§ Rotation and recovery).
- A signing key (
ml-dsa-65, 1952-byte public key) delegated by the root and used for every signature outside the KEL.
ACE ID#
aceId = "ace:sha256:" || lowercaseHex(SHA-256(icpBytes))
icpBytes are the canonical bytes of the inception event. The ID never changes: rotation and re-delegation append events.
Key event log#
Every event carries t, seq (0, then +1 per event), ts (wire integer, non-decreasing, ts <= now + TIMESTAMP_WINDOW_SECONDS) and, after the inception, prev (64 lowercase hex: SHA-256 of the previous event's canonical bytes). Objects are closed. next and fallback are 64 lowercase hex; publicKey members are canonical Base64 (02).
t | Members | Signed by | Effect |
|---|---|---|---|
icp | root{scheme,publicKey}, next, fallback?, sig | the root | creates the identity; commits the next root and, optionally, a fallback root |
rot | prev, root{scheme,publicKey}, next, fallback?, sig | the new root alone | SHA-256(UTF-8(scheme) ‖ 0x00 ‖ publicKey) of the new root MUST equal the committed next (a primary rot) or the committed fallback (a fallback rot); replaces the root and both commitments; revokes the signing delegation |
dlg | prev, key{scheme,publicKey}, notAfter?, sig | the current root | delegates the signing key, replacing any previous one |
Canonical bytes (encodePayload, decimal):
icp: encodePayload("icp", decimal(seq), decimal(ts), root.scheme, rootPublicKeyBytes, nextDigestBytes[, fallbackDigestBytes])
rot: encodePayload("rot", decimal(seq), decimal(ts), prev, root.scheme, rootPublicKeyBytes, nextDigestBytes[, fallbackDigestBytes])
dlg: encodePayload("dlg", decimal(seq), decimal(ts), prev, key.scheme, keyPublicKeyBytes, notAfterOrEmpty)
prev is the 64-character hex string; key and digest fields are decoded bytes; notAfterOrEmpty is decimal(notAfter) or the empty string. fallbackDigestBytes is a field only when fallback is present; every field is length-prefixed, so the two forms never collide.
sig is {scheme, value} (no seq), REQUIRED on every event and never part of the canonical bytes. It is an ML-DSA signature with action kel over M with aceId, timestamp = ts, an empty seqOrEmpty and payload eventBytes (02); for icp the ACE ID is computed from eventBytes first. sig.scheme MUST equal the scheme of the signing root.
Verifier rules#
A verifier replays the log from seq 0 in this order. Structural failures are invalid_kel; a well-formed signature that does not verify is invalid_signature.
- Structure, before any signature: at most
MAX_KEL_EVENTSevents;icponly atseq0; closed members and field shapes;fallback, when present, differs fromnext; contiguousseq; non-decreasingtswithts <= now + TIMESTAMP_WINDOW_SECONDS;prevequals the hash of the previous event. icp: the root isml-dsa-65with a 1952-byte key; thensigverifies under it.rot: the new root is a validml-dsa-65key; its commitment MUST match thenext(primary) or thefallback(fallback) committed by the lasticporrotbefore it (checked before any signature);sig.schemeequals the new root's scheme;sigverifies under the new root, and the old root takes no part. After arotno signing key is delegated until a newdlg.dlg:sigverifies under the current root, then:key.schemeisml-dsa-65with a 1952-byte key;notAfter, when present, satisfiests < notAfter <= ts + MAX_SIGNING_KEY_LIFETIME. WithoutnotAfterthe key lives until replaced. A newdlgprotects only verifiers that see it: to a verifier shown a log that ends before it,notAfteris the only bound on a stolen signing key, so adlgSHOULD carrynotAfter.- Usable. An identity is usable at time
twhen a signing key is delegated and, ifnotAfteris set,t < notAfter. A relay MUST NOT register an unusable identity and a peer MUST NOT send to one. - Forks. At the first
seqwhere a candidate log and the pinned log differ (the fork point), the candidate supersedes the pinned events from that point only when the candidate's event there is arotand either none of the pinned events at or after it is arot(recovery by the holder of a committed root), or the candidate'srotis primary, the pinned event there is a fallbackrotand no pinned event at or after it reveals the root the candidate'srotreveals (the next root reclaims the identity from the fallback root; a root the pinned log already revealed may be stolen and never reclaims). Otherwise the pinned log is kept and the candidate iskel_conflict. A candidate naming a different ACE ID iskel_conflict. - An endpoint never prunes a pinned log.
Operational signatures#
Every signature outside the KEL is a delegated signature (02). A verifier, using the signer's pinned or freshly verified KEL:
- requires
signature.schemeto beml-dsa-65; - requires
signature.seqto equal theseqof the current signing delegation (stale_peer_bindingotherwise; 06 defines when a receiver refreshes first), and never usesseqto select a key outside that log; - requires signed while live: the object's signed timestamp is at or after the
tsof the delegatingdlgand, ifnotAfteris set, before it (stale_peer_binding); - then verifies the signature (the surface's signature code);
- requires the signer's KEL, after reconciliation with the pin, to be usable at
now(stale_peer_binding).
Before signing, a signer checks its own identity at t = max(now, ts of its last event): the log verifies, the identity is usable at t, and the key and seq are the current delegation. Any failure is the local error identity_unavailable and nothing is signed. t is the signed timestamp (a descriptor or card record also applies the +1 rule of 04).
Pinning#
An endpoint pins, per peer ACE ID, the verified KEL (its seq, last hash and state) and the newest verified descriptor. Under one exclusive lock, a candidate KEL:
- is pinned when there is no pin (
adopted); - replaces the pin when it extends it (
extended) or supersedes it under rule 6 (recovered); - only refreshes
fetchedAtwhen it is a prefix of or equal to the pin (unchanged); - is otherwise
kel_conflictand changes nothing.
Which descriptor is pinned under a log is decided in 04. Every signed document of a pinned identity (descriptor, permit, contact source) is verified under the state that results from reconciling the KEL it carries with the pin: carrying a prefix of the pinned log never reinstates a superseded delegation (stale_peer_binding), and a fork is kel_conflict. Cache expiry triggers a refresh and never removes a pin. A stored pin is re-verified on load; corrupt state is storage_failed. When to pin and refresh is defined by admission (05) and the receive pipeline (06).
Rotation and recovery#
- Signing key compromise: a new root-signed
dlgreplaces the key; signatures with the supersededseqstop verifying wherever the newdlgis seen.notAfterbounds a software key's life. - Root compromise: the thief cannot rotate. The holder of the next root (or of the fallback root) appends a
rotthat forks the log at the start of the current root epoch (after its lasticporrot), revoking every delegation of that epoch, followed by a freshdlg. Peers adopt it asextendedorrecovered(rule 6). - Device loss with the next root kept elsewhere: recover the same way from a copy of the log (for example from a relay).
- Fallback root. An owner who keeps a second, independently stored key commits it as
fallback; either key can then recover alone. Arotthat does not reveal a commitment carries it unchanged: a primaryrotcopies the previousfallbackto keep it, a fallbackrotcopies the previousnextto keep the next root's precedence. Omittingfallbackremoves it. A holder of the fallback root alone knows its own commitments but not the ACE ID; it finds the log through a relay's fallback binding (07), by the first fallback commitment its key chain made, which stays bound however often the chain recovers. The two keys are not symmetric: a fallbackrotcan always be superseded by a primaryrotat the sameseq(rule 6), so the holder of the next root reclaims the identity from a stolen fallback root with a primaryrotat theseqof the thief's fallbackrot, revoking every delegation and commitment made after it, as long as no later event revealed that next root. - A signer MUST refuse to sign a
nextorfallbackthat commits to any root ordlgkey its log has revealed, adlgwhose key equals the current root, and adlgwhose key's commitment equals the currentnextorfallback. A verifier can detect reuse only of keys the log reveals, so these are signer-side refusals. - A signer MUST persist a
rotbefore publishing it, and MUST NOT sign a second event for aseqit has already signed. - Losing the root and every committed root together (the next root, and the fallback root if any), or keeping them all on one lost device, is loss of the identity.
- Next-root compromise is loss of the identity: the thief can rotate, revoke every delegation and commit a next root of its own, and nothing recovers from that, a fallback root included. The next root MUST be stored at least as strongly as the root, and apart from it. Fallback-root compromise lasts until the next root reclaims the identity, as above: until then peers that saw the thief's log accept its signatures. The fallback root MUST be stored apart from both.
Residual risk: a peer whose pin predates the reveal of a stolen root can be re-forked out of band until it refreshes from a relay holding the full log, and a verifier shown a log ending before a dlg accepts the key it replaced until its notAfter (rule 4). Both are inherent to single-signature pre-rotation without witnesses. A relay that has deleted an unregistered identity's KEL checks re-registrations against a fixed-size horizon instead (07).