Fork of github.com/did-method-plc/did-method-plc
1import { DAY, HOUR, cborEncode } from '@atproto/common'
2import * as plc from '@did-plc/lib'
3import { ServerError } from './error'
4import {
5 extractPrefixedBytes,
6 extractMultikey,
7 parseDidKey,
8} from '@atproto/crypto'
9
10const MAX_OP_BYTES = 4000
11const MAX_AKA_ENTRIES = 10
12const MAX_AKA_LENGTH = 258 // max handle length (253) plus at:// prefix (5)
13const MAX_ROTATION_ENTRIES = 10
14const MAX_SERVICE_ENTRIES = 10
15const MAX_SERVICE_TYPE_LENGTH = 256
16const MAX_SERVICE_ENDPOINT_LENGTH = 512
17const MAX_VERIFICATION_METHOD_ENTRIES = 10
18const MAX_ID_LENGTH = 32
19const MAX_DID_KEY_LENGTH = 256 // k256 = 57, BLS12-381 = 143
20
21export function validateIncomingOp(input: unknown): plc.OpOrTombstone {
22 const byteLength = cborEncode(input).byteLength
23 if (byteLength > MAX_OP_BYTES) {
24 throw new ServerError(
25 400,
26 `Operation too large (${MAX_OP_BYTES} bytes maximum in cbor encoding)`,
27 )
28 }
29
30 // We *need* to parse, and use the result of the parsing, to ensure that any
31 // unknown fields are removed from the input. "@atproto/common"'s check
32 // function will not remove unknown fields.
33 const result = plc.def.opOrTombstone.safeParse(input)
34
35 if (!result.success) {
36 const errors = result.error.errors.map(
37 (e) => `${e.message} at /${e.path.join('/')}`,
38 )
39 throw new ServerError(
40 400,
41 errors.length
42 ? errors.join('. ') + '.'
43 : `Not a valid operation: ${JSON.stringify(input)}`,
44 )
45 }
46
47 const op = result.data
48
49 if (op.type === 'plc_tombstone') {
50 return op
51 }
52 if (op.alsoKnownAs.length > MAX_AKA_ENTRIES) {
53 throw new ServerError(
54 400,
55 `To many alsoKnownAs entries (max ${MAX_AKA_ENTRIES})`,
56 )
57 }
58 const akaDupe = new Set<string>()
59 for (const aka of op.alsoKnownAs) {
60 if (aka.length > MAX_AKA_LENGTH) {
61 throw new ServerError(
62 400,
63 `alsoKnownAs entry too long (max ${MAX_AKA_LENGTH}): ${aka}`,
64 )
65 }
66 if (akaDupe.has(aka)) {
67 throw new ServerError(400, `duplicate alsoKnownAs entry: ${aka}`)
68 } else {
69 akaDupe.add(aka)
70 }
71 }
72 if (op.rotationKeys.length > MAX_ROTATION_ENTRIES) {
73 throw new ServerError(
74 400,
75 `Too many rotationKey entries (max ${MAX_ROTATION_ENTRIES})`,
76 )
77 }
78 for (const key of op.rotationKeys) {
79 try {
80 parseDidKey(key)
81 } catch (err) {
82 throw new ServerError(400, `Invalid rotationKey: ${key}`)
83 }
84 }
85 const serviceEntries = Object.entries(op.services)
86 if (serviceEntries.length > MAX_SERVICE_ENTRIES) {
87 throw new ServerError(
88 400,
89 `To many service entries (max ${MAX_SERVICE_ENTRIES})`,
90 )
91 }
92 for (const [id, service] of serviceEntries) {
93 if (id.length > MAX_ID_LENGTH) {
94 throw new ServerError(
95 400,
96 `Service id too long (max ${MAX_ID_LENGTH}): ${id}`,
97 )
98 }
99 if (service.type.length > MAX_SERVICE_TYPE_LENGTH) {
100 throw new ServerError(
101 400,
102 `Service type too long (max ${MAX_SERVICE_TYPE_LENGTH})`,
103 )
104 }
105 if (service.endpoint.length > MAX_SERVICE_ENDPOINT_LENGTH) {
106 throw new ServerError(
107 400,
108 `Service endpoint too long (max ${MAX_SERVICE_ENDPOINT_LENGTH})`,
109 )
110 }
111 }
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 }
119 for (const [id, key] of verifyMethods) {
120 if (id.length > MAX_ID_LENGTH) {
121 throw new ServerError(
122 400,
123 `Verification Method id too long (max ${MAX_ID_LENGTH}): ${id}`,
124 )
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 }
132 try {
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
137 } catch (err) {
138 throw new ServerError(400, `Invalid verificationMethod key: ${key}`)
139 }
140 }
141
142 return op
143}
144
145const HOUR_LIMIT = 10
146const DAY_LIMIT = 30
147const WEEK_LIMIT = 100
148
149export const enforceOpsRateLimit = (ops: plc.IndexedOperation[]) => {
150 const hourAgo = new Date(Date.now() - HOUR)
151 const dayAgo = new Date(Date.now() - DAY)
152 const weekAgo = new Date(Date.now() - DAY * 7)
153 let withinHour = 0
154 let withinDay = 0
155 let withinWeek = 0
156 for (const op of ops) {
157 if (op.createdAt > weekAgo) {
158 withinWeek++
159 if (withinWeek >= WEEK_LIMIT) {
160 throw new ServerError(
161 400,
162 `To many operations within last week (max ${WEEK_LIMIT})`,
163 )
164 }
165 }
166 if (op.createdAt > dayAgo) {
167 withinDay++
168 if (withinDay >= DAY_LIMIT) {
169 throw new ServerError(
170 400,
171 `To many operations within last day (max ${DAY_LIMIT})`,
172 )
173 }
174 }
175 if (op.createdAt > hourAgo) {
176 withinHour++
177 if (withinHour >= HOUR_LIMIT) {
178 throw new ServerError(
179 400,
180 `To many operations within last hour (max ${HOUR_LIMIT})`,
181 )
182 }
183 }
184 }
185}