Key Rotation, Recovery, and Deactivation in did:btcr2

Aug 14, 2026

Introduction

Twenty-one posts into this series and we have covered creating identifiers, updating them, resolving them, and anchoring them to Bitcoin. What we have not covered is the part every operator eventually cares about most: what happens when a key has to change.

Keys change for boring reasons and alarming ones. An employee leaves. A signing device is replaced on a hardware refresh cycle. A laptop is stolen. A key was generated on a machine that turned out to be compromised months ago. Any identity system that cannot handle these is a demo, not infrastructure.

This post covers how did:btcr2 handles rotation, what recovery looks like given the method's design, and what deactivation means when the underlying history is immutable by construction. Some of the answers are less comfortable than the marketing version of decentralized identity suggests, which is exactly why they are worth writing down.

btcr2 Has No Recovery Key

Start with the thing that surprises people coming from Sidetree-based methods.

Sidetree, and therefore ION, uses a two-tier key model. Each DID has an update key for routine document changes and a separate recovery key for the emergency case. The recovery key can replace the update key. The intended operational pattern is that the update key lives somewhere convenient and the recovery key lives in a safe, so that compromise of the convenient key is survivable.

did:btcr2 does not work this way. There is no distinguished update key and no distinguished recovery key. An update is authorized by invoking a ZCAP root capability, identified as urn:zcap:root:${encodeURIComponent(did)} (the DID's colons are percent-encoded, so the URN reads urn:zcap:root:did%3Abtcr2%3A...), and the invocation is signed with a verification method listed in the DID document's capabilityInvocation relationship using the bip340-jcs-2025 cryptosuite.

That is the whole authorization model. If a key is in capabilityInvocation, it can authorize any update, including updates that add or remove other keys. If it is not, it cannot authorize updates at all, regardless of what other verification relationships it appears in. A key that is only in authentication can log you into things but cannot touch the document.

The tradeoff is real in both directions and worth stating plainly. The btcr2 model is simpler: one authorization concept, one place to look to answer "who can change this document." The Sidetree model offers a built-in break-glass tier that btcr2 leaves to the operator to construct. Neither is strictly better, but they fail differently, and the failure modes are what you plan around.

Rotation Mechanics

A rotation is an ordinary update. There is no special operation type. You patch the document to replace the key material and the update is authorized like any other.

A BTCR2 Signed Update carries a JSON Patch describing the change, a sourceHash, a targetHash, a targetVersionId, and the Data Integrity proof. The hashes are SHA-256 over the JCS-canonicalized document before and after the patch, which is what lets a verifier confirm it applied the patch to the document the update's author actually had in hand.

A minimal rotation patch replaces one verification method with another:

[
  {
    "op": "replace",
    "path": "/verificationMethod/0/publicKeyMultibase",
    "value": "zQ3shxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
]

Adding a key before removing the old one is usually the safer sequence, and it is a different patch:

[
  {
    "op": "add",
    "path": "/verificationMethod/-",
    "value": {
      "id": "did:btcr2:k1q...#key-2",
      "type": "Multikey",
      "controller": "did:btcr2:k1q...",
      "publicKeyMultibase": "zQ3shxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  },
  {
    "op": "add",
    "path": "/capabilityInvocation/-",
    "value": "did:btcr2:k1q...#key-2"
  }
]

Note the verification method ids in that patch: absolute (did:btcr2:k1q...#key-2), not a bare #key-2 fragment. Use the absolute form, as every example in the spec does. The resolve algorithm requires that the verification method's id match update.proof.verificationMethod and that capabilityInvocation contain that same value, so a document that mixes bare relative fragments with the conventional absolute proof reference fails authorization with INVALID_DID_UPDATE even though the verification method itself is present. That is precisely the lockout the add-then-remove sequence is meant to avoid.

Then, once the new key is confirmed working, a second update removes did:btcr2:k1q...#key-1. Two updates cost two Beacon Signals, but the overlap window means a mistake in the new key material does not lock you out.

The Update That Rotates a Key Is Signed by the Key It Replaces

This is the detail that trips up implementers, and it follows directly from how the resolver works.

The resolve algorithm fixes the order, and the reference implementation follows it across its resolution loop and Resolver.applyUpdate(): hash the current document and confirm it matches the update's declared sourceHash, dereference the root capability and check its invocation target and controller (a derivation the spec leaves as a MAY, which the reference implementation performs), get the verification method from the current document, confirm that method is actually in capabilityInvocation, build the multikey and cryptosuite, canonicalize, verify the proof, then apply the JSON Patch, then compare the result against the update's declared targetHash.

Verify before patch. Never patch before verify.

The consequence is that a rotation update is verified against the key set that exists before the rotation takes effect. The old key signs its own replacement. The new key has no authority over the update that introduces it, and only becomes usable for the update after that one.

This is the correct design. If the new key could authorize its own introduction, anyone could introduce a key. But it has two practical implications that are easy to miss:

A lost key cannot sign its own replacement. If the private key is gone, some other authorized key has to publish the update that removes it, and if no other key is authorized there is no side channel. This is the central constraint of the whole model, and the recovery section below is entirely about preparing for it in advance rather than reacting after the fact.

Historical updates stay verifiable against historical keys. A resolver replaying the document from genesis verifies each update against the key set that was current at that point, not the current key set. Rotating a key does not invalidate the updates the old key signed. That is what makes the history auditable years later, and it is also why a compromised key remains a permanent part of the record rather than something you can erase.

Recovery Without a Recovery Key

So a key is compromised. An attacker has a private key that is in your capabilityInvocation set. What now?

Be clear about the position you are in: the attacker can do exactly what you can do. They can patch the document, they can add their own keys, they can remove yours. There is no tier of authority above them because btcr2 does not define one. You are in a race, and the mechanism that decides the race is the resolver's ordering rules.

Updates are sorted by targetVersionId ascending, with block height as a tiebreaker (the payload carries no height of its own; anchoring is external, through Beacon Signals, and the resolver derives the height during resolution).

The spec is precise about which anomaly produces which error, and it is worth getting right because the three cases are often collapsed into one:

  • A gap in the version sequence, meaning targetVersionId > current_version_id + 1, raises LATE_PUBLISHING.
  • A second update claiming a version already applied goes to a duplicate check: the resolver strips the proof, hashes the update, and compares it against the entry it recorded in update_hash_history. If the hashes differ, it is not a duplicate but a competing history, and that also raises LATE_PUBLISHING.
  • A sourceHash that does not match the document the resolver holds raises INVALID_DID_UPDATE.

All three abort resolution rather than guessing.

That matters more than it might appear. If you and the attacker both publish an update claiming to be version 5, the resolver does not silently pick one and hand the attacker your identity. Whichever landed in the earlier block applies, the other fails the update_hash_history comparison, and resolution halts with LATE_PUBLISHING. The late-publishing machinery exists precisely to reject an inconsistent history rather than resolve it arbitrarily.

The honest reading of that is mixed. It is genuinely good that a compromise cannot silently transfer your DID to someone else while resolution continues to appear normal. It is genuinely bad that the outcome may instead be a DID that no longer resolves cleanly for anyone. A contested rotation can brick an identifier. For a DID that anchors issued credentials, that is a serious operational event, not a minor one.

Which means the real recovery strategy is structural and happens before the compromise:

Multiple keys in capabilityInvocation, held under different custody. If your laptop key and a key on a hardware signer in a drawer are both authorized, compromise of the laptop key leaves you with an authorized key the attacker does not have. You can publish a removal update. You are still in a race, but you are in it with a key they cannot use.

Monitor your Beacon Addresses. The spend history of your Beacon Addresses is public. Anyone can watch it, including you. On a Singleton Beacon every spend is one of your own updates, so an unexpected one is the earliest possible signal that something is wrong, usually well before you would notice any other way. On an aggregated beacon the address is shared and its spends routinely announce other participants' updates, so what you are watching for there is an announcement naming your DID that you did not submit. If you run your own indexed node, this is a cheap watch to set up, and on a Singleton Beacon it is among the highest-value monitoring available for a btcr2 deployment.

Keep the authorized set small and current. Every key in capabilityInvocation is a full-authority key. A set that has accumulated entries over three years, some belonging to people who have left, is a larger attack surface than the document's owner probably realizes. Rotation hygiene is mostly a matter of removing things.

Decide your fork policy in advance. If a contested history does occur, someone has to decide whether to abandon the identifier and reissue credentials under a new one. That decision is much easier to make in a planning document than during an incident.

Deactivation

Sometimes the right end state is that the identifier stops being usable. A company dissolves. A project sunsets. A DID was created for a purpose that has concluded.

Deactivation in btcr2 is, again, an update: a final one whose JSON Patch adds the property deactivated with the value true to the DID document. The reference implementation exposes it as a first-class CLI operation alongside create, resolve, and update, and the patch it sends is literally [{ "op": "add", "path": "/deactivated", "value": true }]. Once a resolver sees deactivated: true it goes no further: the spec declares that state permanent and requires resolution to terminate there, and the returned didDocumentMetadata carries deactivated: true from then on.

Note what this is not. Deactivation is not emptying capabilityInvocation. A document whose authorized key set has been reduced to nothing is frozen, because no key remains to sign an invocation of the root capability, but it is not deactivated by the method's definition, and a conformant resolver will keep resolving it and report deactivated as false.

The semantics deserve care, because "deactivated" does not mean what people usually assume.

The identifier is not deleted. Nothing is removed from Bitcoin. The history remains, permanently, and remains verifiable. Deactivation is a terminal state in the document's lifecycle, not an erasure.

The past remains provable. A credential issued at version 4 and signed by a key that was valid at version 4 still verifies afterward, because a resolver can resolve the DID at that historical version. This is usually what you want, and it is worth confirming your verifiers actually implement historical resolution rather than only resolving to the latest version. A verifier that only checks the current document will treat every credential from a deactivated DID as unverifiable, which is a different and much blunter policy than most issuers intend.

Nothing recovers from it. There is no undo, but the irreversibility is normative rather than cryptographic. Your authorized keys survive deactivation and can still sign; what changes is that a conformant resolver stops the moment it reads deactivated: true, and the spec treats that state as permanent. Test it on regtest first.

The interaction with credential lifecycle is the part most likely to bite. If a deactivated DID issued credentials that are still supposed to be valid, verifiers need a policy for that case, and the policy should be written down before deactivation rather than discovered afterward. If those credentials are not supposed to remain valid, deactivating the issuer DID is not the mechanism that revokes them. That is a separate problem with a separate answer, which is where this series goes in two weeks.

What This Costs

Rotation is an update, so it costs what an update costs: one Beacon Signal. Under a Singleton Beacon that is a dedicated on-chain transaction. Under an aggregated beacon it is a share of one. The add-then-remove pattern costs two.

The more interesting cost is operational rather than monetary. Every rotation produces update data that every verifier will eventually need in order to resolve past your rotation. Distribute it as sidecar data and you carry retention and delivery yourself; publish it to a CAS and you shift retrieval to the resolver but take on that store's availability instead. Rotate frequently and you accumulate history that has to be retained, delivered, and kept intact indefinitely. That obligation is easy to overlook while it is small, and it does not shrink. It is the subject of a later post in this series.

Conclusion

btcr2's rotation model is coherent: one authorization concept, verification against the key set that was current at signing time, and a canonical history that stays auditable. The absence of a dedicated recovery key is a real simplification and a real cost, and the cost lands entirely on operators who did not plan for compromise before it happened.

The planning is not complicated. Authorize more than one key. Hold them under different custody. Watch your Beacon Addresses. Keep the authorized set trimmed. Decide in advance what you will do if the history forks. None of this requires new protocol machinery; it requires deciding to do it while nothing is on fire.

Next week: what "different custody" actually looks like for an organization, including where the hardware signing story for BIP340 is solid and where it still has gaps.

Jintek LLC