Fork of github.com/did-method-plc/did-method-plc

Merge pull request #101 from DavidBuchanan314/permissive-service-keys

Relax constraints on verificationMethod key formats

authored by bnewbold.net and committed by

GitHub 4e2063bc bd582558

+149 -46
+17 -5
packages/lib/src/document.ts
··· 12 12 const verificationMethods: VerificationMethod[] = [] 13 13 for (const [keyid, key] of Object.entries(data.verificationMethods)) { 14 14 const info = formatKeyAndContext(key) 15 - if (!context.includes(info.context)) { 15 + if (info.context && !context.includes(info.context)) { 16 16 context.push(info.context) 17 17 } 18 18 verificationMethods.push({ ··· 55 55 } 56 56 57 57 type KeyAndContext = { 58 - context: string 58 + context?: string 59 59 type: string 60 - publicKeyMultibase 60 + publicKeyMultibase: string 61 61 } 62 62 63 63 const formatKeyAndContext = (key: string): KeyAndContext => { ··· 65 65 try { 66 66 keyInfo = crypto.parseDidKey(key) 67 67 } catch (err) { 68 - throw new UnsupportedKeyError(key, err) 68 + return { 69 + // we can't specify a context for a key type we don't recognize 70 + type: 'Multikey', 71 + publicKeyMultibase: key.replace(/^(did:key:)/, ''), 72 + } 69 73 } 70 74 const { jwtAlg } = keyInfo 71 75 ··· 82 86 publicKeyMultibase: key.replace(/^(did:key:)/, ''), 83 87 } 84 88 } 85 - throw new UnsupportedKeyError(key, `Unsupported key type: ${jwtAlg}`) 89 + 90 + // this codepath might seem unreachable/redundant, but it's possible 91 + // parseDidKey() supports more key formats in future, before this function 92 + // can be updated likewise 93 + return { 94 + // we can't specify a context for a key type we don't recognize 95 + type: 'Multikey', 96 + publicKeyMultibase: key.replace(/^(did:key:)/, ''), 97 + } 86 98 }
+23 -2
packages/server/src/constraints.ts
··· 1 1 import { DAY, HOUR, cborEncode } from '@atproto/common' 2 2 import * as plc from '@did-plc/lib' 3 3 import { ServerError } from './error' 4 - import { parseDidKey } from '@atproto/crypto' 4 + import { 5 + extractPrefixedBytes, 6 + extractMultikey, 7 + parseDidKey, 8 + } from '@atproto/crypto' 5 9 6 10 const MAX_OP_BYTES = 4000 7 11 const MAX_AKA_ENTRIES = 10 ··· 10 14 const MAX_SERVICE_ENTRIES = 10 11 15 const MAX_SERVICE_TYPE_LENGTH = 256 12 16 const MAX_SERVICE_ENDPOINT_LENGTH = 512 17 + const MAX_VERIFICATION_METHOD_ENTRIES = 10 13 18 const MAX_ID_LENGTH = 32 19 + const MAX_DID_KEY_LENGTH = 256 // k256 = 57, BLS12-381 = 143 14 20 15 21 export function validateIncomingOp(input: unknown): plc.OpOrTombstone { 16 22 const byteLength = cborEncode(input).byteLength ··· 104 110 } 105 111 } 106 112 const verifyMethods = Object.entries(op.verificationMethods) 113 + if (verifyMethods.length > MAX_VERIFICATION_METHOD_ENTRIES) { 114 + throw new ServerError( 115 + 400, 116 + `Too many Verification Method entries (max ${MAX_VERIFICATION_METHOD_ENTRIES})`, 117 + ) 118 + } 107 119 for (const [id, key] of verifyMethods) { 108 120 if (id.length > MAX_ID_LENGTH) { 109 121 throw new ServerError( ··· 111 123 `Verification Method id too long (max ${MAX_ID_LENGTH}): ${id}`, 112 124 ) 113 125 } 126 + if (key.length > MAX_DID_KEY_LENGTH) { 127 + throw new ServerError( 128 + 400, 129 + `Verification Method key too long (max ${MAX_DID_KEY_LENGTH}): ${key}`, 130 + ) 131 + } 114 132 try { 115 - parseDidKey(key) 133 + // perform only minimal did:key syntax checking, with no restrictions on 134 + // key types 135 + const multikey = extractMultikey(key) // enforces did:key: prefix 136 + extractPrefixedBytes(multikey) // enforces base58-btc encoding 116 137 } catch (err) { 117 138 throw new ServerError(400, `Invalid verificationMethod key: ${key}`) 118 139 }
+109 -39
packages/server/tests/server.test.ts
··· 6 6 import { didForCreateOp, PlcClientError } from '@did-plc/lib' 7 7 8 8 describe('PLC server', () => { 9 - let handle = 'at://alice.example.com' 9 + let handle1 = 'at://alice.example.com' 10 + let handle2 = 'at://bob.example.com' 10 11 let atpPds = 'https://example.com' 11 12 12 13 let close: CloseFn ··· 16 17 let signingKey: P256Keypair 17 18 let rotationKey1: P256Keypair 18 19 let rotationKey2: P256Keypair 20 + let rotationKey3: P256Keypair 19 21 20 - let did: string 22 + let did1: string 23 + let did2: string 21 24 22 25 beforeAll(async () => { 23 26 const server = await runTestServer({ ··· 30 33 signingKey = await P256Keypair.create() 31 34 rotationKey1 = await P256Keypair.create() 32 35 rotationKey2 = await P256Keypair.create() 36 + rotationKey3 = await P256Keypair.create() 33 37 }) 34 38 35 39 afterAll(async () => { ··· 42 46 if (!doc) { 43 47 throw new Error('expected doc') 44 48 } 45 - expect(doc.did).toEqual(did) 49 + expect(doc.did).toEqual(did1) 46 50 expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() }) 47 51 expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 48 - expect(doc.alsoKnownAs).toEqual([handle]) 52 + expect(doc.alsoKnownAs).toEqual([handle1]) 49 53 expect(doc.services).toEqual({ 50 54 atproto_pds: { 51 55 type: 'AtprotoPersonalDataServer', ··· 55 59 } 56 60 57 61 it('registers a did', async () => { 58 - did = await client.createDid({ 62 + did1 = await client.createDid({ 59 63 signingKey: signingKey.did(), 60 64 rotationKeys: [rotationKey1.did(), rotationKey2.did()], 61 - handle, 65 + handle: handle1, 62 66 pds: atpPds, 63 67 signer: rotationKey1, 64 68 }) 69 + 70 + did2 = await client.createDid({ 71 + signingKey: signingKey.did(), 72 + rotationKeys: [rotationKey3.did()], 73 + handle: handle2, 74 + pds: atpPds, 75 + signer: rotationKey3, 76 + }) 65 77 }) 66 78 67 79 it('retrieves did doc data', async () => { 68 - const doc = await client.getDocumentData(did) 80 + const doc = await client.getDocumentData(did1) 69 81 verifyDoc(doc) 70 82 }) 71 83 72 84 it('can perform some updates', async () => { 73 85 const newRotationKey = await P256Keypair.create() 74 86 signingKey = await P256Keypair.create() 75 - handle = 'at://ali.example2.com' 87 + handle1 = 'at://ali.example2.com' 76 88 atpPds = 'https://example2.com' 77 89 78 - await client.updateAtprotoKey(did, rotationKey1, signingKey.did()) 79 - await client.updateRotationKeys(did, rotationKey1, [ 90 + await client.updateAtprotoKey(did1, rotationKey1, signingKey.did()) 91 + await client.updateRotationKeys(did1, rotationKey1, [ 80 92 newRotationKey.did(), 81 93 rotationKey2.did(), 82 94 ]) 83 95 rotationKey1 = newRotationKey 84 96 85 - await client.updateHandle(did, rotationKey1, handle) 86 - await client.updatePds(did, rotationKey1, atpPds) 97 + await client.updateHandle(did1, rotationKey1, handle1) 98 + await client.updatePds(did1, rotationKey1, atpPds) 87 99 88 - const doc = await client.getDocumentData(did) 100 + const doc = await client.getDocumentData(did1) 89 101 verifyDoc(doc) 90 102 }) 91 103 92 - it('does not allow key types that we do not support', async () => { 93 - // an ed25519 key which we don't yet support 104 + it('does not allow *rotation* key types that we do not yet support', async () => { 105 + // an ed25519 key, which we don't yet support 106 + const newRotationKey = 107 + 'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9' 108 + 109 + const promise = client.updateRotationKeys(did2, rotationKey3, [ 110 + rotationKey2.did(), 111 + newRotationKey, 112 + ]) 113 + await expect(promise).rejects.toThrow(PlcClientError) 114 + }) 115 + 116 + it('allows *verificationMethod* key types that we do not explicitly support', async () => { 117 + // an ed25519 key, which we don't explicitly support 94 118 const newSigningKey = 95 119 'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9' 96 120 97 - const promise = client.updateAtprotoKey(did, rotationKey1, newSigningKey) 98 - await expect(promise).rejects.toThrow(PlcClientError) 121 + // Note: atproto itself does not currently support ed25519 keys, but PLC 122 + // does not have opinions about atproto (or other services!) 123 + await client.updateAtprotoKey(did2, rotationKey3, newSigningKey) 99 124 100 - const promise2 = client.updateRotationKeys(did, rotationKey1, [ 101 - newSigningKey, 125 + // a BLS12-381 key 126 + const exoticSigningKeyFromTheFuture = 127 + 'did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY' 128 + await client.updateAtprotoKey( 129 + did2, 130 + rotationKey3, 131 + exoticSigningKeyFromTheFuture, 132 + ) 133 + 134 + // check that we can still read back the rendered did document 135 + const doc = await client.getDocument(did2) 136 + expect(doc.verificationMethod).toEqual([ 137 + { 138 + id: did2 + '#atproto', 139 + type: 'Multikey', 140 + controller: did2, 141 + publicKeyMultibase: exoticSigningKeyFromTheFuture.slice(8), 142 + }, 102 143 ]) 144 + }) 145 + 146 + it('does not allow syntactically invalid verificationMethod keys', async () => { 147 + const promise1 = client.updateAtprotoKey( 148 + did2, 149 + rotationKey3, 150 + 'did:key:BJV2WY5DJMJQXGZJANFZSAYLXMVZW63LFEEQFY3ZP', // not b58 (b32!) 151 + ) 152 + await expect(promise1).rejects.toThrow(PlcClientError) 153 + const promise2 = client.updateAtprotoKey( 154 + did2, 155 + rotationKey3, 156 + 'did:banana', // a malformed did:key 157 + ) 103 158 await expect(promise2).rejects.toThrow(PlcClientError) 159 + const promise3 = client.updateAtprotoKey( 160 + did2, 161 + rotationKey3, 162 + 'blah', // an even more malformed did:key 163 + ) 164 + await expect(promise3).rejects.toThrow(PlcClientError) 165 + }) 166 + 167 + it('does not allow unreasonably long verificationMethod keys', async () => { 168 + const promise = client.updateAtprotoKey( 169 + did2, 170 + rotationKey3, 171 + 'did:key:z41vu8qtWtp8XRJ9Te5QhkyzU9ByBbiw7bZHKXDjZ8iYorixqZQmEZpxgVSteYirYWMBjqQuEbMYTDsCzXXCAanCSH2xG2cwpbCWGZ2coY2PnhbrDVo7QghsAHpm2X5zsRRwDLyUcm9MTNQAZuRs2B22ygQw3UwkKLA7PZ9ZQ9wMHppmkoaBapmUGaxRNjp1Mt4zxrm9RbEx8FiK3ANBL1fsjggNqvkKpbj6MjntRScPQnJCes9Vt1cFe3iwNP7Ya9RfbaKsVi1eothvSBcbWoouHActGeakHgqFLj1JpbkP7PL3hGGSWLQbXxzmdrfzBCYAtiUxGRvpf3JiaNA2WYbJTh58bzx', 172 + ) 173 + await expect(promise).rejects.toThrow(PlcClientError) 104 174 }) 105 175 106 176 it('retrieves the operation log', async () => { 107 - const doc = await client.getDocumentData(did) 108 - const ops = await client.getOperationLog(did) 109 - const computedDoc = await plc.validateOperationLog(did, ops) 177 + const doc = await client.getDocumentData(did1) 178 + const ops = await client.getOperationLog(did1) 179 + const computedDoc = await plc.validateOperationLog(did1, ops) 110 180 expect(computedDoc).toEqual(doc) 111 181 }) 112 182 113 183 it('rejects on bad updates', async () => { 114 184 const newKey = await P256Keypair.create() 115 - const operation = client.updateAtprotoKey(did, newKey, newKey.did()) 185 + const operation = client.updateAtprotoKey(did1, newKey, newKey.did()) 116 186 await expect(operation).rejects.toThrow() 117 187 }) 118 188 119 189 it('allows for recovery through a forked history', async () => { 120 190 const attackerKey = await P256Keypair.create() 121 - await client.updateRotationKeys(did, rotationKey2, [attackerKey.did()]) 191 + await client.updateRotationKeys(did1, rotationKey2, [attackerKey.did()]) 122 192 123 193 const newKey = await P256Keypair.create() 124 - const ops = await client.getOperationLog(did) 194 + const ops = await client.getOperationLog(did1) 125 195 const forkPoint = ops.at(-2) 126 196 if (!check.is(forkPoint, plc.def.operation)) { 127 197 throw new Error('Could not find fork point') ··· 130 200 rotationKey1.did(), 131 201 newKey.did(), 132 202 ]) 133 - await client.sendOperation(did, op) 203 + await client.sendOperation(did1, op) 134 204 135 205 rotationKey2 = newKey 136 206 137 - const doc = await client.getDocumentData(did) 207 + const doc = await client.getDocumentData(did1) 138 208 verifyDoc(doc) 139 209 }) 140 210 141 211 it('retrieves the auditable operation log', async () => { 142 - const log = await client.getOperationLog(did) 143 - const auditable = await client.getAuditableLog(did) 212 + const log = await client.getOperationLog(did1) 213 + const auditable = await client.getAuditableLog(did1) 144 214 // has one nullifed op 145 215 expect(auditable.length).toBe(log.length + 1) 146 216 expect(auditable.filter((op) => op.nullified).length).toBe(1) ··· 151 221 }) 152 222 153 223 it('retrieves the did doc', async () => { 154 - const data = await client.getDocumentData(did) 155 - const doc = await client.getDocument(did) 224 + const data = await client.getDocumentData(did1) 225 + const doc = await client.getDocument(did1) 156 226 expect(doc).toEqual(plc.formatDidDoc(data)) 157 227 }) 158 228 ··· 188 258 await Promise.all( 189 259 keys.map(async (key) => { 190 260 try { 191 - await client.updateAtprotoKey(did, rotationKey1, key.did()) 261 + await client.updateAtprotoKey(did1, rotationKey1, key.did()) 192 262 successes++ 193 263 } catch (err) { 194 264 failures++ ··· 198 268 expect(successes).toBe(1) 199 269 expect(failures).toBe(19) 200 270 201 - const ops = await client.getOperationLog(did) 202 - await plc.validateOperationLog(did, ops) 271 + const ops = await client.getOperationLog(did1) 272 + await plc.validateOperationLog(did1, ops) 203 273 }) 204 274 205 275 it('tombstones the did', async () => { 206 - await client.tombstone(did, rotationKey1) 276 + await client.tombstone(did1, rotationKey1) 207 277 208 - const promise = client.getDocument(did) 278 + const promise = client.getDocument(did1) 209 279 await expect(promise).rejects.toThrow(PlcClientError) 210 - const promise2 = client.getDocumentData(did) 280 + const promise2 = client.getDocumentData(did1) 211 281 await expect(promise2).rejects.toThrow(PlcClientError) 212 282 }) 213 283 214 284 it('exports the data set', async () => { 215 285 const data = await client.export() 216 286 expect(data.every((row) => check.is(row, plc.def.exportedOp))).toBeTruthy() 217 - expect(data.length).toBe(29) 287 + expect(data.length).toBe(32) 218 288 for (let i = 1; i < data.length; i++) { 219 289 expect(data[i].createdAt >= data[i - 1].createdAt).toBeTruthy() 220 290 } ··· 226 296 type: 'create', 227 297 signingKey: signingKey.did(), 228 298 recoveryKey: rotationKey1.did(), 229 - handle, 299 + handle: handle1, 230 300 service: atpPds, 231 301 prev: null, 232 302 },