Fork of github.com/did-method-plc/did-method-plc
1import { cidForCbor, DAY, HOUR } from '@atproto/common' 2import { P256Keypair, Keypair, Secp256k1Keypair } from '@atproto/crypto' 3import { CID } from 'multiformats/cid' 4import { InvalidSignatureError, LateRecoveryError } from '../src' 5import * as data from '../src/data' 6import * as operations from '../src/operations' 7import * as t from '../src/types' 8 9describe('plc recovery', () => { 10 let signingKey: Secp256k1Keypair 11 let rotationKey1: Secp256k1Keypair 12 let rotationKey2: P256Keypair 13 let rotationKey3: P256Keypair 14 let did: string 15 const handle = 'alice.example.com' 16 const atpPds = 'https://example.com' 17 18 let log: t.IndexedOperation[] = [] 19 20 let createCid: CID 21 22 beforeAll(async () => { 23 signingKey = await Secp256k1Keypair.create() 24 rotationKey1 = await Secp256k1Keypair.create() 25 rotationKey2 = await P256Keypair.create() 26 rotationKey3 = await P256Keypair.create() 27 }) 28 29 const formatIndexed = async ( 30 op: t.Operation, 31 ): Promise<t.IndexedOperation> => { 32 const cid = await cidForCbor(op) 33 34 return { 35 did, 36 operation: op, 37 cid, 38 nullified: false, 39 createdAt: new Date(), 40 } 41 } 42 43 const signOpForKeys = async ( 44 keys: Keypair[], 45 prev: CID | null, 46 signer: Keypair, 47 otherChanges: Partial<t.Operation> = {}, 48 ) => { 49 const unsigned = { 50 ...operations.formatAtprotoOp({ 51 signingKey: signingKey.did(), 52 rotationKeys: keys.map((k) => k.did()), 53 handle, 54 pds: atpPds, 55 prev, 56 }), 57 ...otherChanges, 58 } 59 const op = await operations.addSignature(unsigned, signer) 60 const indexed = await formatIndexed(op) 61 return { op, indexed } 62 } 63 64 it('creates an op log with rotation', async () => { 65 const create = await signOpForKeys( 66 [rotationKey1, rotationKey2, rotationKey3], 67 null, 68 rotationKey1, 69 ) 70 createCid = create.indexed.cid 71 72 log.push({ 73 ...create.indexed, 74 createdAt: new Date(Date.now() - 7 * DAY), 75 }) 76 77 // key 3 tries to usurp control 78 const rotate = await signOpForKeys([rotationKey3], createCid, rotationKey3) 79 80 log.push({ 81 ...rotate.indexed, 82 createdAt: new Date(Date.now() - DAY), 83 }) 84 85 // and does some additional ops 86 const another = await signOpForKeys( 87 [rotationKey3], 88 rotate.indexed.cid, 89 rotationKey3, 90 { alsoKnownAs: ['newhandle.test'] }, 91 ) 92 93 log.push({ 94 ...another.indexed, 95 createdAt: new Date(Date.now() - HOUR), 96 }) 97 }) 98 99 it('allows a rotation key with higher authority to rewrite history', async () => { 100 // key 2 asserts control over key 3 101 const rotate = await signOpForKeys([rotationKey2], createCid, rotationKey2) 102 103 const res = await data.assureValidNextOp(did, log, rotate.op) 104 expect(res.nullified.length).toBe(2) 105 expect(res.nullified[0].equals(log[1].cid)) 106 expect(res.nullified[1].equals(log[2].cid)) 107 expect(res.prev?.equals(createCid)).toBeTruthy() 108 109 log = [log[0], rotate.indexed] 110 }) 111 112 it('does not allow the lower authority key to take control back', async () => { 113 const rotate = await signOpForKeys([rotationKey3], createCid, rotationKey3) 114 await expect(data.assureValidNextOp(did, log, rotate.op)).rejects.toThrow( 115 InvalidSignatureError, 116 ) 117 }) 118 119 it('allows a rotation key with even higher authority to rewrite history', async () => { 120 const rotate = await signOpForKeys([rotationKey1], createCid, rotationKey1) 121 122 const res = await data.assureValidNextOp(did, log, rotate.op) 123 expect(res.nullified.length).toBe(1) 124 expect(res.nullified[0].equals(log[1].cid)) 125 expect(res.prev?.equals(createCid)).toBeTruthy() 126 127 log = [log[0], rotate.indexed] 128 }) 129 130 it('does not allow the either invalidated key to take control back', async () => { 131 const rotate1 = await signOpForKeys([rotationKey3], createCid, rotationKey3) 132 await expect(data.assureValidNextOp(did, log, rotate1.op)).rejects.toThrow( 133 InvalidSignatureError, 134 ) 135 136 const rotate2 = await signOpForKeys([rotationKey2], createCid, rotationKey2) 137 await expect(data.assureValidNextOp(did, log, rotate2.op)).rejects.toThrow( 138 InvalidSignatureError, 139 ) 140 }) 141 142 it('does not allow recovery outside of 72 hrs', async () => { 143 const rotate = await signOpForKeys([rotationKey3], createCid, rotationKey3) 144 const timeOutOps = [ 145 log[0], 146 { 147 ...rotate.indexed, 148 createdAt: new Date(Date.now() - 4 * DAY), 149 }, 150 ] 151 const rotateBack = await signOpForKeys( 152 [rotationKey2], 153 createCid, 154 rotationKey2, 155 ) 156 await expect( 157 data.assureValidNextOp(did, timeOutOps, rotateBack.op), 158 ).rejects.toThrow(LateRecoveryError) 159 }) 160 161 it('allows recovery from a tombstoned DID', async () => { 162 const tombstone = await operations.tombstoneOp(createCid, rotationKey2) 163 const cid = await cidForCbor(tombstone) 164 const tombstoneOps = [ 165 log[0], 166 { 167 did, 168 operation: tombstone, 169 cid, 170 nullified: false, 171 createdAt: new Date(), 172 }, 173 ] 174 const rotateBack = await signOpForKeys( 175 [rotationKey1], 176 createCid, 177 rotationKey1, 178 ) 179 const result = await data.assureValidNextOp( 180 did, 181 tombstoneOps, 182 rotateBack.op, 183 ) 184 expect(result.nullified.length).toBe(1) 185 expect(result.nullified[0].equals(cid)) 186 expect(result.prev?.equals(createCid)).toBeTruthy() 187 }) 188})