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})