OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer commits / 548c8ea

scripts/rotation-keys: append the new rotation key, never prepend it
The first version placed the new key at index 0. That is backwards and the audit against keet caught it. Lower index = higher authority: a recovery fork must be signed by a key with a STRICTLY LOWER index than the one it overrides (did-plc S4). Ranking a fresh offline key first makes it supreme -- nothing can fork against it, so a photograph of that paper ends the identity with no recourse. That is exactly the unrecoverable shape keet has (single terminal root, no rotation, no revocation, entropy escrow designed and abandoned) and precisely what did:plc's ranked rotationKeys exist to avoid. Keeping the incumbent root at [0] preserves its power to fork back if the backup is ever stolen, while the backup at [1] still fixes the single-key LOSS problem that froze wj62. Both failure modes covered; neither traded for the other. Re-verified against loser-club (dry run, nothing submitted): local validation PASS, resulting order [0] incumbent root, [1] new backup. Refs: hyper-social #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
548c8eab565257d78ef40d0410cf5600359589b5
by plankatron <fleeky@gmail.com> · 2026-08-17T19:13:38.000Z

Changed files (1) +15 -7

scripts/rotation-keys.mjs +15 -7
……@@ -26,9 +26,16 @@
2626// head, touching nothing. `POST /:did` is a PERMANENT, IRREVERSIBLE append to the global PLC
2727// op-log — there is no delete and no undo.
2828//
29// ORDERING: the new key is placed FIRST. Lower index = higher authority in a dispute (did-plc
30// §4: a recovery fork must be signed by a key with a LOWER index than the one it overrides). The
31// existing key is preserved, so current capability is never reduced — only added to.
29// ORDERING: the new key is APPENDED, never prepended. Lower index = higher authority (did-plc §4:
30// a recovery fork must be signed by a key with a STRICTLY LOWER index than the one it overrides).
31//
32// It is tempting to rank a fresh offline key first "because it is the safest one". That is
33// backwards. A key at [0] is supreme: nothing can fork against it, so a photograph of that paper
34// ends the identity with no recourse — precisely the unrecoverable shape keet has and did:plc
35// exists to avoid. Keeping the incumbent root at [0] preserves its power to fork back if the
36// backup is ever stolen, while the backup at [1] still fixes the single-key LOSS problem.
37//
38// The existing key is always preserved, so capability is only ever added to, never reduced.
3239//
3340// ⚠ Do not register a key you have not recorded. An unbacked-up rotation key is the exact
3441// failure this tool exists to prevent, and registering one merely moves the single point.
……@@ -148,9 +155,10 @@ async function cmdAdd () {
148155 const signer = usable[0]
149156 console.log(`\nsigning with: [${signer.i}] ${signer.s.label}`)
150157
151 // New key FIRST — highest authority. Everything else carried forward unchanged: a PLC op
152 // replaces the whole document state, it is not a patch.
153 const rotationDidKeys = [addKey, ...current]
158 // New key APPENDED — the incumbent root keeps index 0 and therefore keeps fork authority over
159 // the backup. Everything else carried forward unchanged: a PLC op replaces the whole document
160 // state, it is not a patch.
161 const rotationDidKeys = [...current, addKey]
154162 const unsigned = buildUpdateOp({
155163 prevSignedOp: head,
156164 rotationDidKeys,
……@@ -164,7 +172,7 @@ async function cmdAdd () {
164172 const chk = checkUpdateOp(signedOp, head)
165173 console.log(`\nlocal validation: ${chk.ok ? 'PASS' : 'FAIL — ' + chk.error}`)
166174 console.log('\nresulting rotationKeys:')
167 rotationDidKeys.forEach((k, i) => console.log(` [${i}] ${k}${k === addKey ? ' <-- NEW (highest authority)' : ''}`))
175 rotationDidKeys.forEach((k, i) => console.log(` [${i}] ${k}${k === addKey ? ' <-- NEW backup (lower authority than [0], by design)' : ''}`))
168176 console.log('\nop to be submitted:')
169177 console.log(JSON.stringify(signedOp, null, 2))
170178