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}