OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-identity / test / self-custody.test.js

56 lines · javascript ref:
// selfCustodyIdentity: the atproto-correct onboarding — a deterministic root-derived genesis
// pins the DID (reproducible, device-independent), then a post-genesis rotation attests the
// per-device signing key. Contrast device-attestation.test.js (buildAttestedIdentity), where a
// different device yields a different DID. This is the P0b fix (atproto-base/DESIGN.md §4a).

import test from 'brittle'
import {
  selfCustodyIdentity, createAtprotoIdentity, deriveAtprotoKey, generateAtprotoKey, verifyOp
} from '../lib/atproto-identity.js'
import { didForCreateOp, unsignedOp, opCid } from '../lib/did-plc.js'

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'
const OPTS = { handle: 'p2p.7rnx.net', pdsUrl: 'https://pds.7rnx.net' }

test('DID is reproducible + device-independent (= the deterministic genesis)', (t) => {
  const genesisDid = createAtprotoIdentity(ROOT, OPTS).did
  const a = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS, deviceKey: deriveAtprotoKey(DEVICE).privateKey })
  const b = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS, deviceKey: generateAtprotoKey().privateKey })
  t.is(a.did, genesisDid, 'DID = the root-derived deterministic genesis DID')
  t.is(b.did, genesisDid, 'a DIFFERENT device key yields the SAME DID (device-independent)')
  t.is(didForCreateOp(a.genesisOp), a.did, 'DID hashes from the genesis op')
})

test('genesis vm = root; rotation vm = device; chained + root-signed', (t) => {
  const id = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS, deviceKey: deriveAtprotoKey(DEVICE).privateKey })
  const root = deriveAtprotoKey(ROOT)
  const device = deriveAtprotoKey(DEVICE)

  t.is(id.genesisOp.verificationMethods.atproto, root.didKey, 'genesis vm = ROOT-derived key')
  t.is(id.genesisOp.prev, null, 'genesis prev:null')
  t.is(id.rotationOp.verificationMethods.atproto, device.didKey, 'rotation vm = DEVICE key')
  t.is(id.rotationOp.prev, opCid(id.genesisOp), 'rotation chains onto the genesis (prev)')
  t.alike(id.rotationOp.rotationKeys, id.genesisOp.rotationKeys, 'rotationKeys carried forward (root keeps authority)')

  // both ops signed by the ROOT (a rotationKey), not the device
  t.ok(verifyOp(unsignedOp(id.genesisOp), id.genesisOp.sig, root.publicKey), 'genesis signed by root')
  t.ok(verifyOp(unsignedOp(id.rotationOp), id.rotationOp.sig, root.publicKey), 'rotation signed by root')
  t.absent(verifyOp(unsignedOp(id.rotationOp), id.rotationOp.sig, device.publicKey), 'rotation NOT signed by device')
})

test('peer holds the DEVICE key (root-independent signing) + root stays public-only', (t) => {
  const id = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS, deviceKey: deriveAtprotoKey(DEVICE).privateKey })
  const device = deriveAtprotoKey(DEVICE)
  t.is(id.deviceKey.privateKey.length, 32, 'device key is a usable 32-byte signing key')
  t.is(id.deviceKey.didKey, device.didKey, 'device key matches the rotation vm')
  t.not(id.deviceKey.didKey, id.rootDidKey, 'device signing key ≠ root rotation key (separation)')
})

test('minted device key (no deviceKey arg) still yields the same DID', (t) => {
  const genesisDid = createAtprotoIdentity(ROOT, OPTS).did
  const id = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS })
  t.is(id.did, genesisDid, 'auto-minted device key does not change the DID')
  t.ok(id.deviceKey.privateKey && id.deviceKey.privateKey.length === 32, 'a fresh device key was minted')
})