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

Configure Feed

Select the types of activity you want to include in your feed.

Enforce new operation constraints (#47)

* add additional constraints on new ops

* prevent dupe akas

* tidy

* enforce ops limits

* tidy

authored by

Daniel Holmgren and committed by
GitHub
2baba9b5 30358fa9

+177 -57
-8
packages/lib/src/data.ts
··· 3 3 import * as t from './types' 4 4 import { 5 5 assureValidCreationOp, 6 - assureValidOp, 7 6 assureValidSig, 8 7 normalizeOp, 9 8 } from './operations' ··· 18 17 ops: t.IndexedOperation[], 19 18 proposed: t.CompatibleOpOrTombstone, 20 19 ): Promise<{ nullified: CID[]; prev: CID | null }> => { 21 - if (check.is(proposed, t.def.createOpV1)) { 22 - const normalized = normalizeOp(proposed) 23 - await assureValidOp(normalized) 24 - } else { 25 - await assureValidOp(proposed) 26 - } 27 - 28 20 // special case if account creation 29 21 if (ops.length === 0) { 30 22 await assureValidCreationOp(did, proposed)
+1 -25
packages/lib/src/operations.ts
··· 1 1 import * as cbor from '@ipld/dag-cbor' 2 2 import { CID } from 'multiformats/cid' 3 3 import * as uint8arrays from 'uint8arrays' 4 - import { Keypair, parseDidKey, sha256, verifySignature } from '@atproto/crypto' 4 + import { Keypair, sha256, verifySignature } from '@atproto/crypto' 5 5 import { check, cidForCbor } from '@atproto/common' 6 6 import * as t from './types' 7 7 import { ··· 9 9 ImproperOperationError, 10 10 InvalidSignatureError, 11 11 MisorderedOperationError, 12 - UnsupportedKeyError, 13 12 } from './error' 14 13 15 14 export const didForCreateOp = async (op: t.CompatibleOp) => { ··· 240 239 // Verifying operations/signatures 241 240 // --------------------------- 242 241 243 - export const assureValidOp = async (op: t.OpOrTombstone) => { 244 - if (check.is(op, t.def.tombstone)) { 245 - return true 246 - } 247 - // ensure we support the op's keys 248 - const keys = [...Object.values(op.verificationMethods), ...op.rotationKeys] 249 - await Promise.all( 250 - keys.map(async (k) => { 251 - try { 252 - parseDidKey(k) 253 - } catch (err) { 254 - throw new UnsupportedKeyError(k, err) 255 - } 256 - }), 257 - ) 258 - if (op.rotationKeys.length > 5) { 259 - throw new ImproperOperationError('too many rotation keys', op) 260 - } else if (op.rotationKeys.length < 1) { 261 - throw new ImproperOperationError('need at least one rotation key', op) 262 - } 263 - } 264 - 265 242 export const assureValidCreationOp = async ( 266 243 did: string, 267 244 op: t.CompatibleOpOrTombstone, ··· 270 247 throw new MisorderedOperationError() 271 248 } 272 249 const normalized = normalizeOp(op) 273 - await assureValidOp(normalized) 274 250 await assureValidSig(normalized.rotationKeys, op) 275 251 const expectedDid = await didForCreateOp(op) 276 252 if (expectedDid !== did) {
+18 -14
packages/lib/src/types.ts
··· 34 34 const createOpV1 = unsignedCreateOpV1.extend({ sig: z.string() }) 35 35 export type CreateOpV1 = z.infer<typeof createOpV1> 36 36 37 - const unsignedOperation = z.object({ 38 - type: z.literal('plc_operation'), 39 - rotationKeys: z.array(z.string()), 40 - verificationMethods: z.record(z.string()), 41 - alsoKnownAs: z.array(z.string()), 42 - services: z.record(service), 43 - prev: z.string().nullable(), 44 - }) 37 + const unsignedOperation = z 38 + .object({ 39 + type: z.literal('plc_operation'), 40 + rotationKeys: z.array(z.string()), 41 + verificationMethods: z.record(z.string()), 42 + alsoKnownAs: z.array(z.string()), 43 + services: z.record(service), 44 + prev: z.string().nullable(), 45 + }) 46 + .strict() 45 47 export type UnsignedOperation = z.infer<typeof unsignedOperation> 46 - const operation = unsignedOperation.extend({ sig: z.string() }) 48 + const operation = unsignedOperation.extend({ sig: z.string() }).strict() 47 49 export type Operation = z.infer<typeof operation> 48 50 49 - const unsignedTombstone = z.object({ 50 - type: z.literal('plc_tombstone'), 51 - prev: z.string(), 52 - }) 51 + const unsignedTombstone = z 52 + .object({ 53 + type: z.literal('plc_tombstone'), 54 + prev: z.string(), 55 + }) 56 + .strict() 53 57 export type UnsignedTombstone = z.infer<typeof unsignedTombstone> 54 - const tombstone = unsignedTombstone.extend({ sig: z.string() }) 58 + const tombstone = unsignedTombstone.extend({ sig: z.string() }).strict() 55 59 export type Tombstone = z.infer<typeof tombstone> 56 60 57 61 const opOrTombstone = z.union([operation, tombstone])
+147
packages/server/src/constraints.ts
··· 1 + import { DAY, HOUR, cborEncode, check } from '@atproto/common' 2 + import * as plc from '@did-plc/lib' 3 + import { ServerError } from './error' 4 + import { parseDidKey } from '@atproto/crypto' 5 + 6 + const MAX_OP_BYTES = 4000 7 + const MAX_AKA_ENTRIES = 10 8 + const MAX_AKA_LENGTH = 256 9 + const MAX_ROTATION_ENTRIES = 10 10 + const MAX_SERVICE_ENTRIES = 10 11 + const MAX_SERVICE_TYPE_LENGTH = 256 12 + const MAX_SERVICE_ENDPOINT_LENGTH = 512 13 + const MAX_ID_LENGTH = 32 14 + 15 + export function assertValidIncomingOp( 16 + op: unknown, 17 + ): asserts op is plc.OpOrTombstone { 18 + const byteLength = cborEncode(op).byteLength 19 + if (byteLength > MAX_OP_BYTES) { 20 + throw new ServerError( 21 + 400, 22 + `Operation too large (${MAX_OP_BYTES} bytes maximum in cbor encoding)`, 23 + ) 24 + } 25 + if (!check.is(op, plc.def.opOrTombstone)) { 26 + throw new ServerError(400, `Not a valid operation: ${JSON.stringify(op)}`) 27 + } 28 + if (op.type === 'plc_tombstone') { 29 + return 30 + } 31 + if (op.alsoKnownAs.length > MAX_AKA_ENTRIES) { 32 + throw new ServerError( 33 + 400, 34 + `To many alsoKnownAs entries (max ${MAX_AKA_ENTRIES})`, 35 + ) 36 + } 37 + const akaDupe: Record<string, boolean> = {} 38 + for (const aka of op.alsoKnownAs) { 39 + if (aka.length > MAX_AKA_LENGTH) { 40 + throw new ServerError( 41 + 400, 42 + `alsoKnownAs entry too long (max ${MAX_AKA_LENGTH}): ${aka}`, 43 + ) 44 + } 45 + if (akaDupe[aka]) { 46 + throw new ServerError(400, `duplicate alsoKnownAs entry: ${aka}`) 47 + } else { 48 + akaDupe[aka] = true 49 + } 50 + } 51 + if (op.rotationKeys.length > MAX_ROTATION_ENTRIES) { 52 + throw new ServerError( 53 + 400, 54 + `Too many rotationKey entries (max ${MAX_ROTATION_ENTRIES})`, 55 + ) 56 + } 57 + for (const key of op.rotationKeys) { 58 + try { 59 + parseDidKey(key) 60 + } catch (err) { 61 + throw new ServerError(400, `Invalid rotationKey: ${key}`) 62 + } 63 + } 64 + const serviceEntries = Object.entries(op.services) 65 + if (serviceEntries.length > MAX_SERVICE_ENTRIES) { 66 + throw new ServerError( 67 + 400, 68 + `To many service entries (max ${MAX_SERVICE_ENTRIES})`, 69 + ) 70 + } 71 + for (const [id, service] of serviceEntries) { 72 + if (id.length > MAX_ID_LENGTH) { 73 + throw new ServerError( 74 + 400, 75 + `Service id too long (max ${MAX_ID_LENGTH}): ${id}`, 76 + ) 77 + } 78 + if (service.type.length > MAX_SERVICE_TYPE_LENGTH) { 79 + throw new ServerError( 80 + 400, 81 + `Service type too long (max ${MAX_SERVICE_TYPE_LENGTH})`, 82 + ) 83 + } 84 + if (service.endpoint.length > MAX_SERVICE_ENDPOINT_LENGTH) { 85 + throw new ServerError( 86 + 400, 87 + `Service endpoint too long (max ${MAX_SERVICE_ENDPOINT_LENGTH})`, 88 + ) 89 + } 90 + } 91 + const verifyMethods = Object.entries(op.verificationMethods) 92 + for (const [id, key] of verifyMethods) { 93 + if (id.length > MAX_ID_LENGTH) { 94 + throw new ServerError( 95 + 400, 96 + `Verification Method id too long (max ${MAX_ID_LENGTH}): ${id}`, 97 + ) 98 + } 99 + try { 100 + parseDidKey(key) 101 + } catch (err) { 102 + throw new ServerError(400, `Invalid verificationMethod key: ${key}`) 103 + } 104 + } 105 + } 106 + 107 + const HOUR_LIMIT = 10 108 + const DAY_LIMIT = 30 109 + const WEEK_LIMIT = 100 110 + 111 + export const enforceOpsRateLimit = (ops: plc.IndexedOperation[]) => { 112 + const hourAgo = new Date(Date.now() - HOUR) 113 + const dayAgo = new Date(Date.now() - DAY) 114 + const weekAgo = new Date(Date.now() - DAY * 7) 115 + let withinHour = 0 116 + let withinDay = 0 117 + let withinWeek = 0 118 + for (const op of ops) { 119 + if (op.createdAt > weekAgo) { 120 + withinWeek++ 121 + if (withinWeek >= WEEK_LIMIT) { 122 + throw new ServerError( 123 + 400, 124 + `To many operations within last week (max ${WEEK_LIMIT})`, 125 + ) 126 + } 127 + } 128 + if (op.createdAt > dayAgo) { 129 + withinDay++ 130 + if (withinDay >= DAY_LIMIT) { 131 + throw new ServerError( 132 + 400, 133 + `To many operations within last day (max ${DAY_LIMIT})`, 134 + ) 135 + } 136 + } 137 + if (op.createdAt > hourAgo) { 138 + withinHour++ 139 + if (withinHour >= HOUR_LIMIT) { 140 + throw new ServerError( 141 + 400, 142 + `To many operations within last hour (max ${HOUR_LIMIT})`, 143 + ) 144 + } 145 + } 146 + } 147 + }
+6
packages/server/src/db/index.ts
··· 7 7 import * as migrations from '../migrations' 8 8 import { DatabaseSchema, PlcDatabase } from './types' 9 9 import MockDatabase from './mock' 10 + import { enforceOpsRateLimit } from '../constraints' 10 11 11 12 export * from './mock' 12 13 export * from './types' ··· 99 100 const ops = await this.indexedOpsForDid(did) 100 101 // throws if invalid 101 102 const { nullified, prev } = await plc.assureValidNextOp(did, ops, proposed) 103 + // do not enforce rate limits on recovery operations to prevent DDOS by a bad actor 104 + if (nullified.length === 0) { 105 + enforceOpsRateLimit(ops) 106 + } 107 + 102 108 const cid = await cidForCbor(proposed) 103 109 104 110 await this.db.transaction().execute(async (tx) => {
+2 -8
packages/server/src/routes.ts
··· 1 1 import express from 'express' 2 - import { cborEncode, check } from '@atproto/common' 3 2 import * as plc from '@did-plc/lib' 4 3 import { ServerError } from './error' 5 4 import { AppContext } from './context' 5 + import { assertValidIncomingOp } from './constraints' 6 6 7 7 export const createRouter = (ctx: AppContext): express.Router => { 8 8 const router = express.Router() ··· 114 114 router.post('/:did', async function (req, res) { 115 115 const { did } = req.params 116 116 const op = req.body 117 - const byteLength = cborEncode(op).byteLength 118 - if (byteLength > 7500) { 119 - throw new ServerError(400, 'Operation too large') 120 - } 121 - if (!check.is(op, plc.def.compatibleOpOrTombstone)) { 122 - throw new ServerError(400, `Not a valid operation: ${JSON.stringify(op)}`) 123 - } 117 + assertValidIncomingOp(op) 124 118 await ctx.db.validateAndAddOp(did, op) 125 119 res.sendStatus(200) 126 120 })
+3 -2
packages/server/tests/server.test.ts
··· 220 220 } 221 221 }) 222 222 223 - it('still allows create v1s', async () => { 223 + it('disallows create v1s', async () => { 224 224 const createV1 = await plc.deprecatedSignCreate( 225 225 { 226 226 type: 'create', ··· 233 233 signingKey, 234 234 ) 235 235 const did = await didForCreateOp(createV1) 236 - await client.sendOperation(did, createV1 as any) 236 + const attempt = client.sendOperation(did, createV1 as any) 237 + await expect(attempt).rejects.toThrow() 237 238 }) 238 239 239 240 it('healthcheck succeeds when database is available.', async () => {