Fork of github.com/did-method-plc/did-method-plc
1import { check, cidForCbor } from '@atproto/common' 2import { P256Keypair, Secp256k1Keypair } from '@atproto/crypto' 3import * as ui8 from 'uint8arrays' 4import { 5 GenesisHashError, 6 ImproperOperationError, 7 InvalidSignatureError, 8 MisorderedOperationError, 9} from '../src' 10import * as data from '../src/data' 11import * as operations from '../src/operations' 12import * as t from '../src/types' 13 14describe('plc did data', () => { 15 const ops: t.Operation[] = [] 16 17 let signingKey: Secp256k1Keypair 18 let rotationKey1: Secp256k1Keypair 19 let rotationKey2: P256Keypair 20 let did: string 21 let handle = 'at://alice.example.com' 22 let atpPds = 'https://example.com' 23 24 let oldRotationKey1: Secp256k1Keypair 25 26 beforeAll(async () => { 27 signingKey = await Secp256k1Keypair.create() 28 rotationKey1 = await Secp256k1Keypair.create() 29 rotationKey2 = await P256Keypair.create() 30 }) 31 32 const lastOp = () => { 33 const lastOp = ops.at(-1) 34 if (!lastOp) { 35 throw new Error('expected an op on log') 36 } 37 return lastOp 38 } 39 40 const verifyDoc = (doc: t.DocumentData | null) => { 41 if (!doc) { 42 throw new Error('expected doc') 43 } 44 expect(doc.did).toEqual(did) 45 expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() }) 46 expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 47 expect(doc.alsoKnownAs).toEqual([handle]) 48 expect(doc.services).toEqual({ 49 atproto_pds: { 50 type: 'AtprotoPersonalDataServer', 51 endpoint: atpPds, 52 }, 53 }) 54 } 55 56 it('creates a valid create op', async () => { 57 const createOp = await operations.atprotoOp({ 58 signingKey: signingKey.did(), 59 rotationKeys: [rotationKey1.did(), rotationKey2.did()], 60 handle, 61 pds: atpPds, 62 prev: null, 63 signer: rotationKey1, 64 }) 65 const isValid = check.is(createOp, t.def.operation) 66 expect(isValid).toBeTruthy() 67 ops.push(createOp) 68 did = await operations.didForCreateOp(createOp) 69 }) 70 71 it('parses an operation log with no updates', async () => { 72 const doc = await data.validateOperationLog(did, ops) 73 verifyDoc(doc) 74 }) 75 76 it('updates handle', async () => { 77 const noPrefix = 'ali.exampl2.com' 78 handle = `at://${noPrefix}` 79 const op = await operations.updateHandleOp(lastOp(), rotationKey1, noPrefix) 80 ops.push(op) 81 82 const doc = await data.validateOperationLog(did, ops) 83 verifyDoc(doc) 84 }) 85 86 it('updates atpPds', async () => { 87 const noPrefix = 'example2.com' 88 atpPds = `https://${noPrefix}` 89 const op = await operations.updatePdsOp(lastOp(), rotationKey1, noPrefix) 90 ops.push(op) 91 92 const doc = await data.validateOperationLog(did, ops) 93 verifyDoc(doc) 94 }) 95 96 it('rotates signingKey', async () => { 97 const newSigningKey = await Secp256k1Keypair.create() 98 const op = await operations.updateAtprotoKeyOp( 99 lastOp(), 100 rotationKey1, 101 newSigningKey.did(), 102 ) 103 ops.push(op) 104 105 signingKey = newSigningKey 106 107 const doc = await data.validateOperationLog(did, ops) 108 verifyDoc(doc) 109 }) 110 111 it('rotates rotation keys', async () => { 112 const newRotationKey = await Secp256k1Keypair.create() 113 const op = await operations.updateRotationKeysOp(lastOp(), rotationKey1, [ 114 newRotationKey.did(), 115 rotationKey2.did(), 116 ]) 117 ops.push(op) 118 119 oldRotationKey1 = rotationKey1 120 rotationKey1 = newRotationKey 121 122 const doc = await data.validateOperationLog(did, ops) 123 verifyDoc(doc) 124 }) 125 126 it('no longer allows operations from old rotation key', async () => { 127 const op = await operations.updateHandleOp( 128 lastOp(), 129 oldRotationKey1, 130 'at://bob', 131 ) 132 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 133 InvalidSignatureError, 134 ) 135 }) 136 137 it('does not allow operations from the signingKey', async () => { 138 const op = await operations.updateHandleOp(lastOp(), signingKey, 'at://bob') 139 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 140 InvalidSignatureError, 141 ) 142 }) 143 144 it('does not allow padded signatures', async () => { 145 const op = await operations.updateHandleOp(lastOp(), signingKey, 'at://bob') 146 op.sig = ui8.toString(ui8.fromString(op.sig, 'base64url'), 'base64urlpad') 147 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 148 InvalidSignatureError, 149 ) 150 }) 151 152 it('allows for operations from either rotation key', async () => { 153 const newHandle = 'at://ali.example.com' 154 const op = await operations.updateHandleOp( 155 lastOp(), 156 rotationKey2, 157 newHandle, 158 ) 159 ops.push(op) 160 handle = newHandle 161 const doc = await data.validateOperationLog(did, ops) 162 verifyDoc(doc) 163 }) 164 165 it('allows tombstoning a DID', async () => { 166 const last = await data.getLastOpWithCid(ops) 167 const op = await operations.tombstoneOp(last.cid, rotationKey1) 168 const doc = await data.validateOperationLog(did, [...ops, op]) 169 expect(doc).toBe(null) 170 }) 171 172 it('requires operations to be in order', async () => { 173 const op = await operations.updateHandleOp( 174 ops[ops.length - 2], 175 rotationKey1, 176 'at://bob.test', 177 ) 178 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 179 MisorderedOperationError, 180 ) 181 }) 182 183 it('does not allow a create operation in the middle of the log', async () => { 184 const op = await operations.atprotoOp({ 185 signingKey: signingKey.did(), 186 rotationKeys: [rotationKey1.did(), rotationKey2.did()], 187 handle, 188 pds: atpPds, 189 prev: null, 190 signer: rotationKey1, 191 }) 192 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 193 MisorderedOperationError, 194 ) 195 }) 196 197 it('does not allow a tombstone in the middle of the log', async () => { 198 const prev = await cidForCbor(ops[ops.length - 2]) 199 const tombstone = await operations.tombstoneOp(prev, rotationKey1) 200 expect( 201 data.validateOperationLog(did, [ 202 ...ops.slice(0, ops.length - 1), 203 tombstone, 204 ops[ops.length - 1], 205 ]), 206 ).rejects.toThrow(MisorderedOperationError) 207 }) 208 209 it('requires that the did is the hash of the genesis op', async () => { 210 const rest = ops.slice(1) 211 expect(data.validateOperationLog(did, rest)).rejects.toThrow( 212 GenesisHashError, 213 ) 214 }) 215 216 it('requires that the log starts with a create op (no prev)', async () => { 217 const rest = ops.slice(1) 218 const expectedDid = await operations.didForCreateOp(rest[0]) 219 expect(data.validateOperationLog(expectedDid, rest)).rejects.toThrow( 220 ImproperOperationError, 221 ) 222 }) 223})