| … | … | @@ -0,0 +1,55 @@ |
| 1 | // selfCustodyIdentity: the atproto-correct onboarding — a deterministic root-derived genesis |
| 2 | // pins the DID (reproducible, device-independent), then a post-genesis rotation attests the |
| 3 | // per-device signing key. Contrast device-attestation.test.js (buildAttestedIdentity), where a |
| 4 | // different device yields a different DID. This is the P0b fix (atproto-base/DESIGN.md §4a). |
| 5 | |
| 6 | import test from 'brittle' |
| 7 | import { |
| 8 | selfCustodyIdentity, createAtprotoIdentity, deriveAtprotoKey, generateAtprotoKey, verifyOp |
| 9 | } from '../lib/atproto-identity.js' |
| 10 | import { didForCreateOp, unsignedOp, opCid } from '../lib/did-plc.js' |
| 11 | |
| 12 | const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' |
| 13 | const DEVICE = 'legal winner thank year wave sausage worth useful legal winner thank yellow' |
| 14 | const OPTS = { handle: 'p2p.7rnx.net', pdsUrl: 'https://pds.7rnx.net' } |
| 15 | |
| 16 | test('DID is reproducible + device-independent (= the deterministic genesis)', (t) => { |
| 17 | const genesisDid = createAtprotoIdentity(ROOT, OPTS).did |
| 18 | const a = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS, deviceKey: deriveAtprotoKey(DEVICE).privateKey }) |
| 19 | const b = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS, deviceKey: generateAtprotoKey().privateKey }) |
| 20 | t.is(a.did, genesisDid, 'DID = the root-derived deterministic genesis DID') |
| 21 | t.is(b.did, genesisDid, 'a DIFFERENT device key yields the SAME DID (device-independent)') |
| 22 | t.is(didForCreateOp(a.genesisOp), a.did, 'DID hashes from the genesis op') |
| 23 | }) |
| 24 | |
| 25 | test('genesis vm = root; rotation vm = device; chained + root-signed', (t) => { |
| 26 | const id = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS, deviceKey: deriveAtprotoKey(DEVICE).privateKey }) |
| 27 | const root = deriveAtprotoKey(ROOT) |
| 28 | const device = deriveAtprotoKey(DEVICE) |
| 29 | |
| 30 | t.is(id.genesisOp.verificationMethods.atproto, root.didKey, 'genesis vm = ROOT-derived key') |
| 31 | t.is(id.genesisOp.prev, null, 'genesis prev:null') |
| 32 | t.is(id.rotationOp.verificationMethods.atproto, device.didKey, 'rotation vm = DEVICE key') |
| 33 | t.is(id.rotationOp.prev, opCid(id.genesisOp), 'rotation chains onto the genesis (prev)') |
| 34 | t.alike(id.rotationOp.rotationKeys, id.genesisOp.rotationKeys, 'rotationKeys carried forward (root keeps authority)') |
| 35 | |
| 36 | // both ops signed by the ROOT (a rotationKey), not the device |
| 37 | t.ok(verifyOp(unsignedOp(id.genesisOp), id.genesisOp.sig, root.publicKey), 'genesis signed by root') |
| 38 | t.ok(verifyOp(unsignedOp(id.rotationOp), id.rotationOp.sig, root.publicKey), 'rotation signed by root') |
| 39 | t.absent(verifyOp(unsignedOp(id.rotationOp), id.rotationOp.sig, device.publicKey), 'rotation NOT signed by device') |
| 40 | }) |
| 41 | |
| 42 | test('peer holds the DEVICE key (root-independent signing) + root stays public-only', (t) => { |
| 43 | const id = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS, deviceKey: deriveAtprotoKey(DEVICE).privateKey }) |
| 44 | const device = deriveAtprotoKey(DEVICE) |
| 45 | t.is(id.deviceKey.privateKey.length, 32, 'device key is a usable 32-byte signing key') |
| 46 | t.is(id.deviceKey.didKey, device.didKey, 'device key matches the rotation vm') |
| 47 | t.not(id.deviceKey.didKey, id.rootDidKey, 'device signing key ≠ root rotation key (separation)') |
| 48 | }) |
| 49 | |
| 50 | test('minted device key (no deviceKey arg) still yields the same DID', (t) => { |
| 51 | const genesisDid = createAtprotoIdentity(ROOT, OPTS).did |
| 52 | const id = selfCustodyIdentity({ rootMnemonic: ROOT, ...OPTS }) |
| 53 | t.is(id.did, genesisDid, 'auto-minted device key does not change the DID') |
| 54 | t.ok(id.deviceKey.privateKey && id.deviceKey.privateKey.length === 32, 'a fresh device key was minted') |
| 55 | }) |