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:

  1. Rotate its signing key on a defined schedule or in response to a suspected compromise, without invalidating any previously-issued receipts.
  2. 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.
  3. Bound the trust period of a compromised key: receipts issued before the suspected compromise time remain trusted; receipts issued after are explicitly disqualified.
  4. Serve historical keys so an offline verifier with only the receipt (not network access) can still validate via a separately-fetched key history snapshot.
  5. 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:


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:

  1. Resolve K against the substrate's trust-root-history (live fetch, or a cached snapshot the verifier trusts).
  2. Confirm T falls within [active_from_ns, active_until_ns) for K. (If active_until_ns is null, only T >= active_from_ns is required.)
  3. Verify the receipt's signature against the key listed for K.
  4. (Optional, recommended) Walk the rotation chain by fetching each rotation_event_id and verifying that:
    • Each event has both prev_kid and next_kid signatures.
    • The chain of prev_kid → next_kid is contiguous from the genesis key to current_kid.
    • Each event's rotation_at_ns matches the active_until_ns of the retiring key.

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:

  1. Rotate immediately with reason: "suspected_compromise". The new key co-signs the rotation event; the (potentially attacker-held) old key signs first.
  2. Publish the compromise time as rotation_at_ns. This is an upper bound on when receipts could have been forged.
  3. Mark a "trust horizon": the substrate publishes a separate signed statement asserting "receipts issued by kid = K after compromise_suspected_at_ns = T_c are not trusted." Verifiers MUST treat such receipts with issued_at > T_c as 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:

  1. Generates a new key pair (or fetches from HSM).
  2. Constructs the rotation event body per §2.1.
  3. Signs with the previous key.
  4. Signs with the new key.
  5. Appends the signed event to the MMR.
  6. Updates the trust-root history: sets the previous key's active_until_ns to rotation_at_ns, appends a new entry for the new key.
  7. 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


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:

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:

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

These do not block v1.1 ship.


9. References


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).