Substrate Signing-Key Rotation — v1 Specification
Status: v1.1 — drafted 2026-05-05
Spec URI: https://projectmaya.deoscomputing.io/key-rotation/v1
Maintainer: DEOS Computing (did:web:deos.computing)
Companions: /receipts/format/v1, /attestation/api/v1, /interop/scitt/v1
This document is normative for v1.1 substrates. It specifies how a DEOS trust substrate rotates its issuer signing key (the key that signs receipts in the v1.0 path and Receipts in the SCITT profile), how rotation is recorded, and how downstream verifiers validate signatures issued under any key the substrate has ever used.
1. Goals
A v1.1 substrate must be able to:
- Rotate its signing key on a defined schedule or in response to a suspected compromise, without invalidating any previously-issued receipts.
- Prove the rotation chain so a verifier can decide whether a receipt issued at time T was signed by a key the substrate had legitimate authority over at that time.
- Bound the trust period of a compromised key: receipts issued before the suspected compromise time remain trusted; receipts issued after are explicitly disqualified.
- Serve historical keys so an offline verifier with only the receipt (not network access) can still validate via a separately-fetched key history snapshot.
- Match the SCITT profile. The same rotation mechanism applies to the substrate's SCITT TS signing key — the one that signs Receipts per
/interop/scitt/v1§4.1.
A v1.1 substrate is not required to:
- Support threshold signing across multiple HSMs (deferred to v2).
- Rotate the substrate's MMR — the log identity is the
tree_id, distinct from the signing key. Rotating the signing key produces a rotation event that gets logged in the SAME MMR.
2. Rotation event format
A rotation event is a JCS-canonical JSON object signed by both the previous key and the new key. Co-signing prevents a single compromised key from unilaterally rotating to an attacker-controlled key.
2.1 Body
{
"v": 1,
"schema": "deos_substrate_rotation_v1",
"issuer_did": "did:deos:substrate-alpha-1",
"tree_id": "alpha-log",
"prev_kid": "did:deos:substrate-alpha-1#sig-1",
"next_kid": "did:deos:substrate-alpha-1#sig-2",
"next_alg": "ed25519",
"next_public_key_b64u": "<base64url-nopad of next public key bytes>",
"rotation_at_ns": <unix nanoseconds>,
"reason": "scheduled" | "suspected_compromise" | "operator_request"
}
| Field | Required | Notes |
|---|---|---|
v |
yes | MUST equal 1. |
schema |
yes | MUST equal "deos_substrate_rotation_v1". |
issuer_did |
yes | The substrate's DID. MUST equal prev_kid and next_kid's DID portion (before #). |
tree_id |
yes | The substrate's MMR identifier. The rotation event is anchored in this log. |
prev_kid |
yes | DID URL of the previous key. MUST appear in the substrate's trust-root history. |
next_kid |
yes | DID URL of the new key. MUST be distinct from prev_kid. |
next_alg |
yes | Signing algorithm for the new key. v1 supports "ed25519" only. |
next_public_key_b64u |
yes | Public key of the new key, base64url-nopad encoded. |
rotation_at_ns |
yes | Unix nanoseconds. The new key becomes effective at this time. |
reason |
yes | One of "scheduled", "suspected_compromise", or "operator_request". Implementations MAY define vendor-specific reasons under the x- prefix. |
2.2 Signing
The rotation event is canonicalized per /receipts/format/v1 §4.1, then signed with the domain-separation tag "deos-substrate-rotation-v1:" prepended to the canonical bytes:
canonical_rotation = JCS(rotation_event)
sig_payload = "deos-substrate-rotation-v1:" || canonical_rotation
The event MUST carry two signatures, both Ed25519 over sig_payload:
{
"v": 1,
...
"signatures": [
{ "kid": "<prev_kid>", "alg": "ed25519", "sig": "<base64url-nopad>" },
{ "kid": "<next_kid>", "alg": "ed25519", "sig": "<base64url-nopad>" }
]
}
The signatures array MUST contain a signature by prev_kid AND a signature by next_kid. Verifiers MUST reject events missing either.
2.3 Anchoring
A signed rotation event is appended to the substrate's MMR as a normal log leaf (using the same canonicalization + leaf-hashing as receipts). Its leaf is therefore covered by the substrate's OTS anchor chain, giving the rotation a verifiable timestamp via the same Bitcoin-anchored mechanism that timestamps receipts.
The leaf data is the JCS canonical bytes of the signed rotation event (with both signatures).
3. Trust-root history
3.1 The history endpoint
A v1.1 substrate publishes its key history via:
GET /attestation/v1/trust-root-history
returning:
{
"issuer_did": "did:deos:substrate-alpha-1",
"current_kid": "did:deos:substrate-alpha-1#sig-2",
"keys": [
{
"kid": "did:deos:substrate-alpha-1#sig-1",
"alg": "ed25519",
"public_key_b64u": "<base64url-nopad>",
"active_from_ns": 0,
"active_until_ns": 1746201600000000000,
"rotation_event_id": "<receipt id of the rotation event in the MMR>"
},
{
"kid": "did:deos:substrate-alpha-1#sig-2",
"alg": "ed25519",
"public_key_b64u": "<base64url-nopad>",
"active_from_ns": 1746201600000000000,
"active_until_ns": null,
"rotation_event_id": null
}
]
}
| Field | Notes |
|---|---|
issuer_did |
The substrate's DID. |
current_kid |
The active key id. Equals keys[last].kid. |
keys |
Ordered oldest-first. The current key has active_until_ns: null and rotation_event_id: null. |
keys[i].active_from_ns |
Unix ns. The first key's active_from_ns is 0 (genesis). Each subsequent key's active_from_ns equals the previous key's active_until_ns. |
keys[i].active_until_ns |
Unix ns. Equals the rotation_at_ns of the rotation event that retired this key. null for the current key. |
keys[i].rotation_event_id |
Receipt id of the rotation event in the MMR. Verifiers can fetch the event via GET /attestation/v1/receipt/{id} and audit the rotation chain end-to-end. |
3.2 Verifier obligations
Given a receipt with signatures[].kid = K and issued_at = T, the verifier MUST:
- Resolve
Kagainst the substrate'strust-root-history(live fetch, or a cached snapshot the verifier trusts). - Confirm
Tfalls within[active_from_ns, active_until_ns)forK. (Ifactive_until_nsisnull, onlyT >= active_from_nsis required.) - Verify the receipt's signature against the key listed for
K. - (Optional, recommended) Walk the rotation chain by fetching each
rotation_event_idand verifying that:- Each event has both
prev_kidandnext_kidsignatures. - The chain of
prev_kid → next_kidis contiguous from the genesis key tocurrent_kid. - Each event's
rotation_at_nsmatches theactive_until_nsof the retiring key.
- Each event has both
A receipt signed by a kid not in the trust-root history, or whose issued_at falls outside the key's active window, MUST be rejected.
4. Compromise recovery
If the substrate operator suspects the current signing key has been compromised:
- Rotate immediately with
reason: "suspected_compromise". The new key co-signs the rotation event; the (potentially attacker-held) old key signs first. - Publish the compromise time as
rotation_at_ns. This is an upper bound on when receipts could have been forged. - Mark a "trust horizon": the substrate publishes a separate signed statement asserting "receipts issued by
kid = Kaftercompromise_suspected_at_ns = T_care not trusted." Verifiers MUST treat such receipts withissued_at > T_cas suspect even though they nominally fall within the (compromised) key's active window.
Concretely the trust horizon is encoded in the rotation event itself by setting rotation_at_ns = T_c. Receipts with issued_at >= T_c signed by K are then OUTSIDE K's active window per §3.2 and rejected automatically.
If the attacker has signed receipts with the compromised key BEFORE the operator publishes the rotation, those receipts pass verification under §3.2. The substrate operator is responsible for separately publishing a list of known-fraudulent receipt ids via /revocation/v1 for verifiers to consult.
5. Rotation operations
5.1 Initial state
A new substrate generates a key on first launch (kid = #sig-1). The trust-root history at this point has exactly one entry:
{ "kid": ".../#sig-1", "active_from_ns": 0, "active_until_ns": null, ... }
5.2 Rotating
Operator runs the substrate's rotate-key CLI subcommand. The substrate:
- Generates a new key pair (or fetches from HSM).
- Constructs the rotation event body per §2.1.
- Signs with the previous key.
- Signs with the new key.
- Appends the signed event to the MMR.
- Updates the trust-root history: sets the previous key's
active_until_nstorotation_at_ns, appends a new entry for the new key. - Updates the substrate's persisted state to use the new key for all subsequent receipts.
The rotation MUST be atomic with respect to receipt signing: the substrate MUST NOT issue a receipt signed by next_kid until the rotation event itself is in the MMR.
5.3 Forbidden patterns
- A substrate MUST NOT skip rotation events. If the current key is
#sig-3and the operator wants to introduce#sig-4, the rotation event must referenceprev_kid = #sig-3. - A substrate MUST NOT publish two contiguous rotation events with the same
prev_kid(i.e. forking the rotation chain). Verifiers walking the history would observe twonext_kidcandidates and MUST reject the substrate as malformed. - The trust-root history MUST be append-only. Rewriting historical entries (e.g. shortening a past key's active window) is detectable by verifiers caching the previous history snapshot and is treated as a substrate compromise.
6. Storage
A v1.1 substrate persists key history at:
<data-dir>/
key.json — current signing key (secret + public + kid + alg)
key-history.json — full append-only history (one entry per key, including the current one)
rotation-events/<id>.json — each signed rotation event as JCS JSON, retrievable by id
The current key.json stays as-is for v1.0 compatibility. The new key-history.json is the source of truth for /trust-root-history.
7. Security considerations
This section satisfies the RFC 3552 requirement.
7.1 Co-signing requirement
The dual-signature requirement (§2.2) prevents a single compromised key from rotating to an attacker-controlled key. An attacker holding prev_kid cannot fabricate a rotation event without ALSO holding the legitimate next_kid private key, which the operator generates fresh.
This does not prevent simultaneous compromise of both keys (e.g. both stored on the same compromised host). HSM/KMS isolation is the recommended mitigation.
7.2 Race between compromise and rotation
If an attacker compromises kid = K at time T_c and the operator rotates at T_r > T_c, receipts forged by the attacker between T_c and T_r will pass verification under §3.2 because they fall within K's active window. The operator's responses are:
- Set
rotation_at_ns = T_c(notT_r) whenreason = "suspected_compromise". This places the entire suspect window outsideK's active period, so attacker-forged receipts withissued_at >= T_care rejected. - Cross-publish a list of known-fraudulent receipt ids via the revocation log.
7.3 Verifier cache poisoning
A verifier that caches trust-root-history and trusts the cache without re-fetching is vulnerable to a malicious substrate-MITM that returns a truncated or extended history. Mitigations:
- Cross-witness via the log-gossip protocol in
/mmr/transparency/v1§3. - The DID document (
did:deos) for the substrate carries a hash of the latest history snapshot; verifiers re-fetch the DID document at a defined cadence and re-validate against the upstream registry.
7.4 Algorithm agility
v1 rotation events are Ed25519-only. v1.2 will admit hybrid Ed25519 + ML-DSA-65 via an alg array in the rotation event and a dual next_public_key_b64u set, matching the COSE algorithm-agility approach in the SCITT profile.
8. Open questions for v1.2
- Should the rotation event also commit to the substrate's MMR root at
rotation_at_ns? This would let verifiers prove "this rotation happened AFTER receipt R" without trusting the substrate's local timekeeping. - Should HSM-backed substrates require a third co-signature from the HSM's vendor key (e.g. AWS KMS attestation)? This raises the bar for compromise but adds vendor lock-in.
- Should rotation events themselves be carried inside the SCITT profile's COSE_Sign1 envelope rather than JCS-JSON? This unifies the two paths but complicates the JSON-only legacy path.
These do not block v1.1 ship.
9. References
/receipts/format/v1— receipt canonicalization + signing/attestation/api/v1— server endpoints/revocation/v1— receipt-id revocation list/interop/scitt/v1— SCITT profile (rotation applies identically)/anchor/v1— OTS anchoring (rotation events ride the same chain)- RFC 8785 — JSON Canonicalization Scheme (JCS)
- RFC 3552 — Security considerations format (§7)
10. Maintainer
DEOS Computing — design questions and conformance reports to github.com/DEOS-Computing.
License: CC BY 4.0 (text), Apache-2.0 (reference code).