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

Changed files
+149 -46
packages
lib
server
+17 -5
packages/lib/src/document.ts
···
const verificationMethods: VerificationMethod[] = []
for (const [keyid, key] of Object.entries(data.verificationMethods)) {
const info = formatKeyAndContext(key)
-
if (!context.includes(info.context)) {
+
if (info.context && !context.includes(info.context)) {
context.push(info.context)
}
verificationMethods.push({
···
}
type KeyAndContext = {
-
context: string
+
context?: string
type: string
-
publicKeyMultibase
+
publicKeyMultibase: string
}
const formatKeyAndContext = (key: string): KeyAndContext => {
···
try {
keyInfo = crypto.parseDidKey(key)
} catch (err) {
-
throw new UnsupportedKeyError(key, err)
+
return {
+
// we can't specify a context for a key type we don't recognize
+
type: 'Multikey',
+
publicKeyMultibase: key.replace(/^(did:key:)/, ''),
+
}
}
const { jwtAlg } = keyInfo
···
publicKeyMultibase: key.replace(/^(did:key:)/, ''),
}
}
-
throw new UnsupportedKeyError(key, `Unsupported key type: ${jwtAlg}`)
+
+
// this codepath might seem unreachable/redundant, but it's possible
+
// parseDidKey() supports more key formats in future, before this function
+
// can be updated likewise
+
return {
+
// we can't specify a context for a key type we don't recognize
+
type: 'Multikey',
+
publicKeyMultibase: key.replace(/^(did:key:)/, ''),
+
}
}
+23 -2
packages/server/src/constraints.ts
···
import { DAY, HOUR, cborEncode } from '@atproto/common'
import * as plc from '@did-plc/lib'
import { ServerError } from './error'
-
import { parseDidKey } from '@atproto/crypto'
+
import {
+
extractPrefixedBytes,
+
extractMultikey,
+
parseDidKey,
+
} from '@atproto/crypto'
const MAX_OP_BYTES = 4000
const MAX_AKA_ENTRIES = 10
···
const MAX_SERVICE_ENTRIES = 10
const MAX_SERVICE_TYPE_LENGTH = 256
const MAX_SERVICE_ENDPOINT_LENGTH = 512
+
const MAX_VERIFICATION_METHOD_ENTRIES = 10
const MAX_ID_LENGTH = 32
+
const MAX_DID_KEY_LENGTH = 256 // k256 = 57, BLS12-381 = 143
export function validateIncomingOp(input: unknown): plc.OpOrTombstone {
const byteLength = cborEncode(input).byteLength
···
}
}
const verifyMethods = Object.entries(op.verificationMethods)
+
if (verifyMethods.length > MAX_VERIFICATION_METHOD_ENTRIES) {
+
throw new ServerError(
+
400,
+
`Too many Verification Method entries (max ${MAX_VERIFICATION_METHOD_ENTRIES})`,
+
)
+
}
for (const [id, key] of verifyMethods) {
if (id.length > MAX_ID_LENGTH) {
throw new ServerError(
···
`Verification Method id too long (max ${MAX_ID_LENGTH}): ${id}`,
)
}
+
if (key.length > MAX_DID_KEY_LENGTH) {
+
throw new ServerError(
+
400,
+
`Verification Method key too long (max ${MAX_DID_KEY_LENGTH}): ${key}`,
+
)
+
}
try {
-
parseDidKey(key)
+
// perform only minimal did:key syntax checking, with no restrictions on
+
// key types
+
const multikey = extractMultikey(key) // enforces did:key: prefix
+
extractPrefixedBytes(multikey) // enforces base58-btc encoding
} catch (err) {
throw new ServerError(400, `Invalid verificationMethod key: ${key}`)
}
+109 -39
packages/server/tests/server.test.ts
···
import { didForCreateOp, PlcClientError } from '@did-plc/lib'
describe('PLC server', () => {
-
let handle = 'at://alice.example.com'
+
let handle1 = 'at://alice.example.com'
+
let handle2 = 'at://bob.example.com'
let atpPds = 'https://example.com'
let close: CloseFn
···
let signingKey: P256Keypair
let rotationKey1: P256Keypair
let rotationKey2: P256Keypair
+
let rotationKey3: P256Keypair
-
let did: string
+
let did1: string
+
let did2: string
beforeAll(async () => {
const server = await runTestServer({
···
signingKey = await P256Keypair.create()
rotationKey1 = await P256Keypair.create()
rotationKey2 = await P256Keypair.create()
+
rotationKey3 = await P256Keypair.create()
})
afterAll(async () => {
···
if (!doc) {
throw new Error('expected doc')
}
-
expect(doc.did).toEqual(did)
+
expect(doc.did).toEqual(did1)
expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() })
expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()])
-
expect(doc.alsoKnownAs).toEqual([handle])
+
expect(doc.alsoKnownAs).toEqual([handle1])
expect(doc.services).toEqual({
atproto_pds: {
type: 'AtprotoPersonalDataServer',
···
}
it('registers a did', async () => {
-
did = await client.createDid({
+
did1 = await client.createDid({
signingKey: signingKey.did(),
rotationKeys: [rotationKey1.did(), rotationKey2.did()],
-
handle,
+
handle: handle1,
pds: atpPds,
signer: rotationKey1,
})
+
+
did2 = await client.createDid({
+
signingKey: signingKey.did(),
+
rotationKeys: [rotationKey3.did()],
+
handle: handle2,
+
pds: atpPds,
+
signer: rotationKey3,
+
})
})
it('retrieves did doc data', async () => {
-
const doc = await client.getDocumentData(did)
+
const doc = await client.getDocumentData(did1)
verifyDoc(doc)
})
it('can perform some updates', async () => {
const newRotationKey = await P256Keypair.create()
signingKey = await P256Keypair.create()
-
handle = 'at://ali.example2.com'
+
handle1 = 'at://ali.example2.com'
atpPds = 'https://example2.com'
-
await client.updateAtprotoKey(did, rotationKey1, signingKey.did())
-
await client.updateRotationKeys(did, rotationKey1, [
+
await client.updateAtprotoKey(did1, rotationKey1, signingKey.did())
+
await client.updateRotationKeys(did1, rotationKey1, [
newRotationKey.did(),
rotationKey2.did(),
])
rotationKey1 = newRotationKey
-
await client.updateHandle(did, rotationKey1, handle)
-
await client.updatePds(did, rotationKey1, atpPds)
+
await client.updateHandle(did1, rotationKey1, handle1)
+
await client.updatePds(did1, rotationKey1, atpPds)
-
const doc = await client.getDocumentData(did)
+
const doc = await client.getDocumentData(did1)
verifyDoc(doc)
})
-
it('does not allow key types that we do not support', async () => {
-
// an ed25519 key which we don't yet support
+
it('does not allow *rotation* key types that we do not yet support', async () => {
+
// an ed25519 key, which we don't yet support
+
const newRotationKey =
+
'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9'
+
+
const promise = client.updateRotationKeys(did2, rotationKey3, [
+
rotationKey2.did(),
+
newRotationKey,
+
])
+
await expect(promise).rejects.toThrow(PlcClientError)
+
})
+
+
it('allows *verificationMethod* key types that we do not explicitly support', async () => {
+
// an ed25519 key, which we don't explicitly support
const newSigningKey =
'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9'
-
const promise = client.updateAtprotoKey(did, rotationKey1, newSigningKey)
-
await expect(promise).rejects.toThrow(PlcClientError)
+
// Note: atproto itself does not currently support ed25519 keys, but PLC
+
// does not have opinions about atproto (or other services!)
+
await client.updateAtprotoKey(did2, rotationKey3, newSigningKey)
-
const promise2 = client.updateRotationKeys(did, rotationKey1, [
-
newSigningKey,
+
// a BLS12-381 key
+
const exoticSigningKeyFromTheFuture =
+
'did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY'
+
await client.updateAtprotoKey(
+
did2,
+
rotationKey3,
+
exoticSigningKeyFromTheFuture,
+
)
+
+
// check that we can still read back the rendered did document
+
const doc = await client.getDocument(did2)
+
expect(doc.verificationMethod).toEqual([
+
{
+
id: did2 + '#atproto',
+
type: 'Multikey',
+
controller: did2,
+
publicKeyMultibase: exoticSigningKeyFromTheFuture.slice(8),
+
},
])
+
})
+
+
it('does not allow syntactically invalid verificationMethod keys', async () => {
+
const promise1 = client.updateAtprotoKey(
+
did2,
+
rotationKey3,
+
'did:key:BJV2WY5DJMJQXGZJANFZSAYLXMVZW63LFEEQFY3ZP', // not b58 (b32!)
+
)
+
await expect(promise1).rejects.toThrow(PlcClientError)
+
const promise2 = client.updateAtprotoKey(
+
did2,
+
rotationKey3,
+
'did:banana', // a malformed did:key
+
)
await expect(promise2).rejects.toThrow(PlcClientError)
+
const promise3 = client.updateAtprotoKey(
+
did2,
+
rotationKey3,
+
'blah', // an even more malformed did:key
+
)
+
await expect(promise3).rejects.toThrow(PlcClientError)
+
})
+
+
it('does not allow unreasonably long verificationMethod keys', async () => {
+
const promise = client.updateAtprotoKey(
+
did2,
+
rotationKey3,
+
'did:key:z41vu8qtWtp8XRJ9Te5QhkyzU9ByBbiw7bZHKXDjZ8iYorixqZQmEZpxgVSteYirYWMBjqQuEbMYTDsCzXXCAanCSH2xG2cwpbCWGZ2coY2PnhbrDVo7QghsAHpm2X5zsRRwDLyUcm9MTNQAZuRs2B22ygQw3UwkKLA7PZ9ZQ9wMHppmkoaBapmUGaxRNjp1Mt4zxrm9RbEx8FiK3ANBL1fsjggNqvkKpbj6MjntRScPQnJCes9Vt1cFe3iwNP7Ya9RfbaKsVi1eothvSBcbWoouHActGeakHgqFLj1JpbkP7PL3hGGSWLQbXxzmdrfzBCYAtiUxGRvpf3JiaNA2WYbJTh58bzx',
+
)
+
await expect(promise).rejects.toThrow(PlcClientError)
})
it('retrieves the operation log', async () => {
-
const doc = await client.getDocumentData(did)
-
const ops = await client.getOperationLog(did)
-
const computedDoc = await plc.validateOperationLog(did, ops)
+
const doc = await client.getDocumentData(did1)
+
const ops = await client.getOperationLog(did1)
+
const computedDoc = await plc.validateOperationLog(did1, ops)
expect(computedDoc).toEqual(doc)
})
it('rejects on bad updates', async () => {
const newKey = await P256Keypair.create()
-
const operation = client.updateAtprotoKey(did, newKey, newKey.did())
+
const operation = client.updateAtprotoKey(did1, newKey, newKey.did())
await expect(operation).rejects.toThrow()
})
it('allows for recovery through a forked history', async () => {
const attackerKey = await P256Keypair.create()
-
await client.updateRotationKeys(did, rotationKey2, [attackerKey.did()])
+
await client.updateRotationKeys(did1, rotationKey2, [attackerKey.did()])
const newKey = await P256Keypair.create()
-
const ops = await client.getOperationLog(did)
+
const ops = await client.getOperationLog(did1)
const forkPoint = ops.at(-2)
if (!check.is(forkPoint, plc.def.operation)) {
throw new Error('Could not find fork point')
···
rotationKey1.did(),
newKey.did(),
])
-
await client.sendOperation(did, op)
+
await client.sendOperation(did1, op)
rotationKey2 = newKey
-
const doc = await client.getDocumentData(did)
+
const doc = await client.getDocumentData(did1)
verifyDoc(doc)
})
it('retrieves the auditable operation log', async () => {
-
const log = await client.getOperationLog(did)
-
const auditable = await client.getAuditableLog(did)
+
const log = await client.getOperationLog(did1)
+
const auditable = await client.getAuditableLog(did1)
// has one nullifed op
expect(auditable.length).toBe(log.length + 1)
expect(auditable.filter((op) => op.nullified).length).toBe(1)
···
})
it('retrieves the did doc', async () => {
-
const data = await client.getDocumentData(did)
-
const doc = await client.getDocument(did)
+
const data = await client.getDocumentData(did1)
+
const doc = await client.getDocument(did1)
expect(doc).toEqual(plc.formatDidDoc(data))
})
···
await Promise.all(
keys.map(async (key) => {
try {
-
await client.updateAtprotoKey(did, rotationKey1, key.did())
+
await client.updateAtprotoKey(did1, rotationKey1, key.did())
successes++
} catch (err) {
failures++
···
expect(successes).toBe(1)
expect(failures).toBe(19)
-
const ops = await client.getOperationLog(did)
-
await plc.validateOperationLog(did, ops)
+
const ops = await client.getOperationLog(did1)
+
await plc.validateOperationLog(did1, ops)
})
it('tombstones the did', async () => {
-
await client.tombstone(did, rotationKey1)
+
await client.tombstone(did1, rotationKey1)
-
const promise = client.getDocument(did)
+
const promise = client.getDocument(did1)
await expect(promise).rejects.toThrow(PlcClientError)
-
const promise2 = client.getDocumentData(did)
+
const promise2 = client.getDocumentData(did1)
await expect(promise2).rejects.toThrow(PlcClientError)
})
it('exports the data set', async () => {
const data = await client.export()
expect(data.every((row) => check.is(row, plc.def.exportedOp))).toBeTruthy()
-
expect(data.length).toBe(29)
+
expect(data.length).toBe(32)
for (let i = 1; i < data.length; i++) {
expect(data[i].createdAt >= data[i - 1].createdAt).toBeTruthy()
}
···
type: 'create',
signingKey: signingKey.did(),
recoveryKey: rotationKey1.did(),
-
handle,
+
handle: handle1,
service: atpPds,
prev: null,
},