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