Fork of github.com/did-method-plc/did-method-plc
1import * as cbor from '@ipld/dag-cbor' 2import { CID } from 'multiformats/cid' 3import * as uint8arrays from 'uint8arrays' 4import { Keypair, sha256, verifySignature } from '@atproto/crypto' 5import { check, cidForCbor } from '@atproto/common' 6import * as t from './types' 7import { 8 GenesisHashError, 9 ImproperOperationError, 10 InvalidSignatureError, 11 MisorderedOperationError, 12} from './error' 13 14export const didForCreateOp = async (op: t.CompatibleOp) => { 15 const hashOfGenesis = await sha256(cbor.encode(op)) 16 const hashB32 = uint8arrays.toString(hashOfGenesis, 'base32') 17 const truncated = hashB32.slice(0, 24) 18 return `did:plc:${truncated}` 19} 20 21// Operations formatting 22// --------------------------- 23 24export const formatAtprotoOp = (opts: { 25 signingKey: string 26 handle: string 27 pds: string 28 rotationKeys: string[] 29 prev: CID | null 30}): t.UnsignedOperation => { 31 return { 32 type: 'plc_operation', 33 verificationMethods: { 34 atproto: opts.signingKey, 35 }, 36 rotationKeys: opts.rotationKeys, 37 alsoKnownAs: [ensureAtprotoPrefix(opts.handle)], 38 services: { 39 atproto_pds: { 40 type: 'AtprotoPersonalDataServer', 41 endpoint: ensureHttpPrefix(opts.pds), 42 }, 43 }, 44 prev: opts.prev?.toString() ?? null, 45 } 46} 47 48export const atprotoOp = async (opts: { 49 signingKey: string 50 handle: string 51 pds: string 52 rotationKeys: string[] 53 prev: CID | null 54 signer: Keypair 55}) => { 56 return addSignature(formatAtprotoOp(opts), opts.signer) 57} 58 59export const createOp = async (opts: { 60 signingKey: string 61 handle: string 62 pds: string 63 rotationKeys: string[] 64 signer: Keypair 65}): Promise<{ op: t.Operation; did: string }> => { 66 const op = await atprotoOp({ ...opts, prev: null }) 67 const did = await didForCreateOp(op) 68 return { op, did } 69} 70 71export const createUpdateOp = async ( 72 lastOp: t.CompatibleOp, 73 signer: Keypair, 74 fn: (normalized: t.UnsignedOperation) => Omit<t.UnsignedOperation, 'prev'>, 75): Promise<t.Operation> => { 76 const prev = await cidForCbor(lastOp) 77 // omit sig so it doesn't accidentally make its way into the next operation 78 // eslint-disable-next-line @typescript-eslint/no-unused-vars 79 const { sig, ...normalized } = normalizeOp(lastOp) 80 const unsigned = await fn(normalized) 81 return addSignature( 82 { 83 ...unsigned, 84 prev: prev.toString(), 85 }, 86 signer, 87 ) 88} 89 90export const createAtprotoUpdateOp = async ( 91 lastOp: t.CompatibleOp, 92 signer: Keypair, 93 opts: Partial<{ 94 signingKey: string 95 handle: string 96 pds: string 97 rotationKeys: string[] 98 }>, 99) => { 100 return createUpdateOp(lastOp, signer, (normalized) => { 101 const updated = { ...normalized } 102 if (opts.signingKey) { 103 updated.verificationMethods = { 104 ...normalized.verificationMethods, 105 atproto: opts.signingKey, 106 } 107 } 108 if (opts.handle) { 109 const formatted = ensureAtprotoPrefix(opts.handle) 110 const handleI = normalized.alsoKnownAs.findIndex((h) => 111 h.startsWith('at://'), 112 ) 113 if (handleI < 0) { 114 updated.alsoKnownAs = [formatted, ...normalized.alsoKnownAs] 115 } else { 116 updated.alsoKnownAs = [ 117 ...normalized.alsoKnownAs.slice(0, handleI), 118 formatted, 119 ...normalized.alsoKnownAs.slice(handleI + 1), 120 ] 121 } 122 } 123 if (opts.pds) { 124 const formatted = ensureHttpPrefix(opts.pds) 125 updated.services = { 126 ...normalized.services, 127 atproto_pds: { 128 type: 'AtprotoPersonalDataServer', 129 endpoint: formatted, 130 }, 131 } 132 } 133 if (opts.rotationKeys) { 134 updated.rotationKeys = opts.rotationKeys 135 } 136 return updated 137 }) 138} 139 140export const updateAtprotoKeyOp = async ( 141 lastOp: t.CompatibleOp, 142 signer: Keypair, 143 signingKey: string, 144): Promise<t.Operation> => { 145 return createAtprotoUpdateOp(lastOp, signer, { signingKey }) 146} 147 148export const updateHandleOp = async ( 149 lastOp: t.CompatibleOp, 150 signer: Keypair, 151 handle: string, 152): Promise<t.Operation> => { 153 return createAtprotoUpdateOp(lastOp, signer, { handle }) 154} 155 156export const updatePdsOp = async ( 157 lastOp: t.CompatibleOp, 158 signer: Keypair, 159 pds: string, 160): Promise<t.Operation> => { 161 return createAtprotoUpdateOp(lastOp, signer, { pds }) 162} 163 164export const updateRotationKeysOp = async ( 165 lastOp: t.CompatibleOp, 166 signer: Keypair, 167 rotationKeys: string[], 168): Promise<t.Operation> => { 169 return createAtprotoUpdateOp(lastOp, signer, { rotationKeys }) 170} 171 172export const tombstoneOp = async ( 173 prev: CID, 174 key: Keypair, 175): Promise<t.Tombstone> => { 176 return addSignature( 177 { 178 type: 'plc_tombstone', 179 prev: prev.toString(), 180 }, 181 key, 182 ) 183} 184 185// Signing operations 186// --------------------------- 187 188export const addSignature = async <T extends Record<string, unknown>>( 189 object: T, 190 key: Keypair, 191): Promise<T & { sig: string }> => { 192 const data = new Uint8Array(cbor.encode(object)) 193 const sig = await key.sign(data) 194 return { 195 ...object, 196 sig: uint8arrays.toString(sig, 'base64url'), 197 } 198} 199 200export const signOperation = async ( 201 op: t.UnsignedOperation, 202 signingKey: Keypair, 203): Promise<t.Operation> => { 204 return addSignature(op, signingKey) 205} 206 207// Backwards compatibility 208// --------------------------- 209 210export const deprecatedSignCreate = async ( 211 op: t.UnsignedCreateOpV1, 212 signingKey: Keypair, 213): Promise<t.CreateOpV1> => { 214 return addSignature(op, signingKey) 215} 216 217export const normalizeOp = (op: t.CompatibleOp): t.Operation => { 218 if (check.is(op, t.def.operation)) { 219 return op 220 } 221 return { 222 type: 'plc_operation', 223 verificationMethods: { 224 atproto: op.signingKey, 225 }, 226 rotationKeys: [op.recoveryKey, op.signingKey], 227 alsoKnownAs: [ensureAtprotoPrefix(op.handle)], 228 services: { 229 atproto_pds: { 230 type: 'AtprotoPersonalDataServer', 231 endpoint: ensureHttpPrefix(op.service), 232 }, 233 }, 234 prev: op.prev, 235 sig: op.sig, 236 } 237} 238 239// Verifying operations/signatures 240// --------------------------- 241 242export const assureValidCreationOp = async ( 243 did: string, 244 op: t.CompatibleOpOrTombstone, 245): Promise<t.DocumentData> => { 246 if (check.is(op, t.def.tombstone)) { 247 throw new MisorderedOperationError() 248 } 249 const normalized = normalizeOp(op) 250 await assureValidSig(normalized.rotationKeys, op) 251 const expectedDid = await didForCreateOp(op) 252 if (expectedDid !== did) { 253 throw new GenesisHashError(expectedDid) 254 } 255 if (op.prev !== null) { 256 throw new ImproperOperationError('expected null prev on create', op) 257 } 258 const { verificationMethods, rotationKeys, alsoKnownAs, services } = 259 normalized 260 return { did, verificationMethods, rotationKeys, alsoKnownAs, services } 261} 262 263export const assureValidSig = async ( 264 allowedDidKeys: string[], 265 op: t.CompatibleOpOrTombstone, 266): Promise<string> => { 267 const { sig, ...opData } = op 268 if (sig.endsWith('=')) { 269 throw new InvalidSignatureError(op) 270 } 271 const sigBytes = uint8arrays.fromString(sig, 'base64url') 272 const dataBytes = new Uint8Array(cbor.encode(opData)) 273 for (const didKey of allowedDidKeys) { 274 const isValid = await verifySignature(didKey, dataBytes, sigBytes) 275 if (isValid) { 276 return didKey 277 } 278 } 279 throw new InvalidSignatureError(op) 280} 281 282// Util 283// --------------------------- 284 285export const ensureHttpPrefix = (str: string): string => { 286 if (str.startsWith('http://') || str.startsWith('https://')) { 287 return str 288 } 289 return `https://${str}` 290} 291 292export const ensureAtprotoPrefix = (str: string): string => { 293 if (str.startsWith('at://')) { 294 return str 295 } 296 const stripped = str.replace('http://', '').replace('https://', '') 297 return `at://${stripped}` 298}