OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-identity / test / device-attestation.test.js

49 lines · javascript ref:
// Device-attested anchoring (atproto side): the genesis op must place the ROOT identity in
// rotationKeys (authority/recovery) and the DEVICE key in verificationMethod (repo
// signing), and be signed by the ROOT — never the device. This is the permanent shape
// baked at genesis, so it must be correct before the one irreversible PLC write.

import test from 'brittle'
import { buildAttestedIdentity, verifyAttestedGenesis } from '../lib/device-attestation.js'
import { deriveAtprotoKey } from '../lib/atproto-identity.js'

// two distinct valid BIP39 vectors → distinct secp256k1 keys (root vs device)
const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const DEVICE = 'legal winner thank year wave sausage worth useful legal winner thank yellow'

test('attested genesis: root=rotationKey, device=verificationMethod, root-signed', (t) => {
  const id = buildAttestedIdentity({
    rootMnemonic: ROOT, deviceMnemonic: DEVICE,
    handle: 'p2p.7rnx.net', pdsUrl: 'https://pds.7rnx.net'
  })
  const op = id.signedOp

  t.ok(id.did.startsWith('did:plc:'), 'did:plc minted from the attested genesis')
  t.not(id.deviceDidKey, id.rootDidKey, 'device signing key ≠ root rotation key (separation)')
  t.is(op.verificationMethods.atproto, id.deviceDidKey, 'verificationMethod = device signing key')
  t.is(op.rotationKeys[0], id.rootDidKey, 'rotationKeys[0] = root (authority / recovery)')
  t.ok(verifyAttestedGenesis(op, id.rootPublicKey), 'genesis is signed by the ROOT rotation key')
  t.absent(verifyAttestedGenesis(op, id.deviceSigningKey.publicKey), 'genesis is NOT signed by the device key')
  t.ok(id.deviceSigningKey.privateKey && id.deviceSigningKey.privateKey.length === 32,
    'device holds a usable 32-byte signing key (it signs the repo commits)')
})

test('extra rotation keys form a recovery chain (root + device both authoritative)', (t) => {
  const dev = deriveAtprotoKey(DEVICE)
  const id = buildAttestedIdentity({
    rootMnemonic: ROOT, deviceMnemonic: DEVICE, extraRotationDidKeys: [dev.didKey],
    handle: 'p2p.7rnx.net', pdsUrl: 'https://pds.7rnx.net'
  })
  const rk = id.signedOp.rotationKeys
  t.is(rk.length, 2, 'two rotation keys (a chain: root + device)')
  t.is(rk[0], id.rootDidKey, 'root has priority (first in the ordered rotation set)')
  t.is(rk[1], dev.didKey, 'device is also a rotation key — it can recover if the root is lost')
})

test('the did:plc changes if the rotation set changes (rotation is baked at genesis)', (t) => {
  const a = buildAttestedIdentity({ rootMnemonic: ROOT, deviceMnemonic: DEVICE, handle: 'p2p.7rnx.net', pdsUrl: 'https://pds.7rnx.net' })
  const dev = deriveAtprotoKey(DEVICE)
  const b = buildAttestedIdentity({ rootMnemonic: ROOT, deviceMnemonic: DEVICE, extraRotationDidKeys: [dev.didKey], handle: 'p2p.7rnx.net', pdsUrl: 'https://pds.7rnx.net' })
  t.not(a.did, b.did, 'a different rotation set yields a different permanent DID — decide it before --publish')
})