OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / rotation-guard.test.js

80 lines · javascript ref:
// rotation-guard — can we still CHANGE each hosted DID? (hyper-social #47)
//
// The failure this guards is invisible by construction: wj62 kept serving perfectly for six weeks with
// its rotation authority already gone, because SERVING needs only the signing key. So the property under
// test is not "does it compute a boolean" but "does it distinguish the three answers that matter":
//
//   true   we hold a rotation key — the DID is still fixable
//   false  we hold none — frozen FOREVER, and no later action undoes it
//   null   we could not tell (no network, encrypted vault)
//
// The null case is the one worth a test of its own. Collapsing "could not check" into "false" is exactly
// the bug the 5A gate shipped (a user told their device was revoked when the check simply could not run),
// and collapsing it the other way hides a dead identity behind a green tick.
//
// fetchImpl is injected, so this is offline and deterministic — it never touches plc.directory.
// Run: bare test/rotation-guard.test.js
import '../lib/bare-globals.js'
import test from 'brittle'
import { deriveAtprotoKey, ATPROTO_SIGNING_PATH, ATPROTO_RECOVERY_PATH } from 'atproto-identity'
import { collectRotationHeld, localRotationCandidates } from '../lib/rotation-guard.js'

const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DID = 'did:plc:rotationguardtest00000'

const signingKey = deriveAtprotoKey(MNEMONIC, ATPROTO_SIGNING_PATH).didKey
const recoveryKey = deriveAtprotoKey(MNEMONIC, ATPROTO_RECOVERY_PATH).didKey
const cfg = { dids: [{ did: DID, deviceMnemonic: MNEMONIC }] }

const plc = (rotationKeys) => async () => ({ ok: true, json: async () => ({ rotationKeys }) })

test('a rotation key we can derive reports held:true and names its source', async (t) => {
  const r = (await collectRotationHeld({ cfg, fetchImpl: plc([recoveryKey]) })).get(DID)
  t.is(r.held, true, 'held')
  t.ok(r.heldBy && r.heldBy.includes('deviceMnemonic'), 'names WHERE the key came from — an operator must be able to find it')
  t.is(r.rotationKeyCount, 1)
})

test('it matches on EITHER derivation path, because genesis used the signing path', async (t) => {
  // buildAttestedIdentity called resolveKey(rootMnemonic) with no path argument, so the root fell through
  // to deriveAtprotoKey's default — ATPROTO_SIGNING_PATH. A check that only tried the recovery path would
  // have reported wj62's own root as absent.
  const r = (await collectRotationHeld({ cfg, fetchImpl: plc([signingKey]) })).get(DID)
  t.is(r.held, true, 'signing-path root is found too')
})

test('a DID whose rotation key we do NOT hold reports held:false — the alarm', async (t) => {
  // Derived from a DIFFERENT BIP39 test vector, so it cannot collide with anything on the machine.
  // This previously hard-coded wj62's real rotation key as the "foreign" example — which passed only
  // while that key was missing from this box, and started failing (correctly!) the moment the vault
  // was restored. A test whose verdict depends on the operator's live secrets is not a test.
  const foreign = deriveAtprotoKey('legal winner thank year wave sausage worth useful legal winner thank yellow', ATPROTO_SIGNING_PATH).didKey
  const r = (await collectRotationHeld({ cfg, fetchImpl: plc([foreign]) })).get(DID)
  t.is(r.held, false, 'false, not null — this is a determined verdict')
  t.ok(r.reason && r.reason.includes('no local secret'), 'and says why')
})

test('an unresolvable DID reports null, NEVER false', async (t) => {
  const dead = async () => { throw new Error('network is down') }
  const r = (await collectRotationHeld({ cfg, fetchImpl: dead })).get(DID)
  t.is(r.held, null, 'a check that could not run is not a NO')
  t.ok(r.reason && r.reason.includes('plc.directory'), 'and reports the cause rather than a bare null')
  t.is(r.rotationKeyCount, null, 'and claims no count it did not observe')
})

test('holding the ONLY rotation key is still flagged as a single point of failure', async (t) => {
  const one = (await collectRotationHeld({ cfg, fetchImpl: plc([recoveryKey]) })).get(DID)
  t.is(one.held, true)
  t.is(one.singleKeyRisk, true, 'held AND fragile are different facts, reported separately')

  const two = (await collectRotationHeld({ cfg, fetchImpl: plc([recoveryKey, signingKey]) })).get(DID)
  t.is(two.singleKeyRisk, false, 'a second key clears the risk')
  t.is(two.rotationKeyCount, 2)
})

test('candidate collection never throws on unusable material', async (t) => {
  const junk = { dids: [{ did: DID, deviceMnemonic: 'not a valid mnemonic at all', signingKey: 'zzzz' }] }
  const { owned } = localRotationCandidates(junk)
  t.ok(owned instanceof Map, 'returns a map rather than throwing — one bad entry must not blind the whole check')
})