This repository has no description
0

Configure Feed

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

vit / plc_register.js
9.7 kB 318 lines
1#!/usr/bin/env bun 2 3import { secp256k1 } from '@noble/curves/secp256k1'; 4import { p256 } from '@noble/curves/p256'; 5import { sha256 } from '@noble/hashes/sha256'; 6import { encode as dagCborEncode } from '@ipld/dag-cbor'; 7import bs58 from 'bs58'; 8import { Command } from 'commander'; 9import { mkdirSync, writeFileSync } from 'node:fs'; 10import { resolve } from 'node:path'; 11 12const MC_P256_PUB = new Uint8Array([0x80, 0x24]); 13const MC_K256_PUB = new Uint8Array([0xe7, 0x01]); 14const N_K256 = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; 15const N_P256 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n; 16 17const K256_ALG_ID = new Uint8Array([ 18 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 19 0x04, 0x00, 0x0a, 20]); 21 22const P256_ALG_ID = new Uint8Array([ 23 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 24 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 25]); 26 27function ensureDir(path) { 28 mkdirSync(path, { recursive: true }); 29} 30 31function b64urlNopad(data) { 32 return Buffer.from(data).toString('base64url').replace(/=+$/g, ''); 33} 34 35function base32LowerNopad(data) { 36 const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'; 37 let bits = 0; 38 let value = 0; 39 let result = ''; 40 for (const byte of data) { 41 value = (value << 8) | byte; 42 bits += 8; 43 while (bits >= 5) { 44 result += alphabet[(value >>> (bits - 5)) & 31]; 45 bits -= 5; 46 } 47 } 48 if (bits > 0) { 49 result += alphabet[(value << (5 - bits)) & 31]; 50 } 51 return result; 52} 53 54function getCurve(curve) { 55 if (curve === 'k256') { 56 return { ec: secp256k1, order: N_K256, mcPrefix: MC_K256_PUB, algId: K256_ALG_ID }; 57 } 58 if (curve === 'p256') { 59 return { ec: p256, order: N_P256, mcPrefix: MC_P256_PUB, algId: P256_ALG_ID }; 60 } 61 throw new Error("curve must be 'k256' or 'p256'"); 62} 63 64function didKeyForPub(curve, compressedPubkey) { 65 const { mcPrefix } = getCurve(curve); 66 const prefixed = new Uint8Array(mcPrefix.length + compressedPubkey.length); 67 prefixed.set(mcPrefix); 68 prefixed.set(compressedPubkey, mcPrefix.length); 69 return 'did:key:z' + bs58.encode(prefixed); 70} 71 72function lowS(curve, r, s) { 73 const { order } = getCurve(curve); 74 if (s > order / 2n) { 75 s = order - s; 76 } 77 return [r, s]; 78} 79 80function bigintToBytes(n, length) { 81 const hex = n.toString(16).padStart(length * 2, '0'); 82 if (hex.length > length * 2) { 83 throw new Error(`integer too large for ${length} bytes`); 84 } 85 const bytes = new Uint8Array(length); 86 for (let i = 0; i < length; i++) { 87 bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); 88 } 89 return bytes; 90} 91 92function signLowSRaw(curve, privateKey, message) { 93 const { ec } = getCurve(curve); 94 const msgHash = sha256(message); 95 const sig = ec.sign(msgHash, privateKey); 96 let [r, s] = [sig.r, sig.s]; 97 [r, s] = lowS(curve, r, s); 98 const raw = new Uint8Array(64); 99 raw.set(bigintToBytes(r, 32), 0); 100 raw.set(bigintToBytes(s, 32), 32); 101 return raw; 102} 103 104function derLength(length) { 105 if (length < 0x80) { 106 return Buffer.from([length]); 107 } 108 if (length <= 0xff) { 109 return Buffer.from([0x81, length]); 110 } 111 return Buffer.from([0x82, (length >>> 8) & 0xff, length & 0xff]); 112} 113 114function derWrap(tag, content) { 115 const body = Buffer.from(content); 116 return Buffer.concat([Buffer.from([tag]), derLength(body.length), body]); 117} 118 119function derSequence(parts) { 120 const content = Buffer.concat(parts.map((part) => Buffer.from(part))); 121 return derWrap(0x30, content); 122} 123 124function derIntegerSmall(n) { 125 if (n === 0) return Buffer.from([0x02, 0x01, 0x00]); 126 if (n === 1) return Buffer.from([0x02, 0x01, 0x01]); 127 throw new Error('unsupported integer'); 128} 129 130function derOctetString(data) { 131 return derWrap(0x04, data); 132} 133 134function derBitString(data) { 135 return derWrap(0x03, Buffer.concat([Buffer.from([0x00]), Buffer.from(data)])); 136} 137 138function buildPkcs8Der(curve, privateKeyBytes, uncompressedPubkey) { 139 const { algId } = getCurve(curve); 140 const ecPrivateKey = derSequence([ 141 derIntegerSmall(1), 142 derOctetString(privateKeyBytes), 143 derWrap(0xa1, derBitString(uncompressedPubkey)), 144 ]); 145 return derSequence([derIntegerSmall(0), algId, derOctetString(ecPrivateKey)]); 146} 147 148function buildSpkiDer(curve, uncompressedPubkey) { 149 const { algId } = getCurve(curve); 150 return derSequence([algId, derBitString(uncompressedPubkey)]); 151} 152 153function toPem(label, der) { 154 const b64 = Buffer.from(der).toString('base64'); 155 const lines = b64.match(/.{1,64}/g) ?? []; 156 return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n`; 157} 158 159function generateRotationKey(outdir, curve, verbose = false) { 160 ensureDir(outdir); 161 const { ec } = getCurve(curve); 162 163 const privateKeyBytes = ec.utils.randomPrivateKey(); 164 const compressed = ec.getPublicKey(privateKeyBytes, true); 165 const uncompressed = ec.getPublicKey(privateKeyBytes, false); 166 167 const privPemPath = `${outdir}/rotation_${curve}_private.pem`; 168 const pubPemPath = `${outdir}/rotation_${curve}_public.pem`; 169 170 const privatePem = toPem('PRIVATE KEY', buildPkcs8Der(curve, privateKeyBytes, uncompressed)); 171 const publicPem = toPem('PUBLIC KEY', buildSpkiDer(curve, uncompressed)); 172 writeFileSync(privPemPath, privatePem); 173 writeFileSync(pubPemPath, publicPem); 174 175 const didKey = didKeyForPub(curve, compressed); 176 177 if (verbose) { 178 console.log(`[verbose] Generated ${curve.toUpperCase()} keypair`); 179 console.log(`[verbose] Compressed pubkey: ${Buffer.from(compressed).toString('hex')}`); 180 } 181 182 return { 183 curve, 184 privPemPath, 185 pubPemPath, 186 didKey, 187 privateKeyBytes, 188 compressed, 189 }; 190} 191 192function buildUnsignedOp(rotationDidKeys, akaList, pdsEndpoint) { 193 const verificationMethods = {}; 194 const services = {}; 195 if (pdsEndpoint) { 196 services.atproto_pds = { 197 type: 'AtprotoPersonalDataServer', 198 endpoint: pdsEndpoint, 199 }; 200 } 201 return { 202 type: 'plc_operation', 203 rotationKeys: rotationDidKeys, 204 verificationMethods: verificationMethods, 205 alsoKnownAs: akaList, 206 services: services, 207 prev: null, 208 }; 209} 210 211function derivePlcDid(signedOp) { 212 const cbor = dagCborEncode(signedOp); 213 const digest = sha256(cbor); 214 const suffix = base32LowerNopad(digest).slice(0, 24); 215 return 'did:plc:' + suffix; 216} 217 218function collect(value, previous) { 219 previous.push(value); 220 return previous; 221} 222 223async function main() { 224 const program = new Command(); 225 program 226 .description('Generate & register a DID:PLC genesis operation.') 227 .option('--out <dir>', 'Output directory for keys (default: plc_keys)', 'plc_keys') 228 .option('--curve <curve>', 'Rotation key curve', 'k256') 229 .option('--aka <uri>', 'alsoKnownAs entry (e.g., at://alice.example). May repeat.', collect, []) 230 .option('--pds <url>', 'PDS endpoint URL (e.g., https://pds.example.com)') 231 .option('--dry-run', 'Build & print but do not POST to PLC') 232 .option('-v, --verbose', 'Show verbose output') 233 .parse(); 234 235 const args = program.opts(); 236 if (!['k256', 'p256'].includes(args.curve)) { 237 console.error(`error: option '--curve' must be 'k256' or 'p256'`); 238 process.exit(1); 239 } 240 const kb = generateRotationKey(args.out, args.curve, args.verbose); 241 const unsigned = buildUnsignedOp([kb.didKey], args.aka, args.pds ?? null); 242 243 if (args.verbose) { 244 console.log('[verbose] Unsigned operation:'); 245 console.log(JSON.stringify(unsigned, null, 2)); 246 } 247 248 const unsignedCbor = dagCborEncode(unsigned); 249 if (args.verbose) { 250 console.log(`[verbose] Encoded CBOR size: ${unsignedCbor.length} bytes`); 251 } 252 253 const rawSig = signLowSRaw(args.curve, kb.privateKeyBytes, unsignedCbor); 254 const sigB64u = b64urlNopad(rawSig); 255 256 if (args.verbose) { 257 console.log(`[verbose] Signature (base64url): ${sigB64u}`); 258 } 259 260 const signed = { ...unsigned, sig: sigB64u }; 261 const did = derivePlcDid(signed); 262 263 if (args.verbose) { 264 const signedCbor = dagCborEncode(signed); 265 const digest = sha256(signedCbor); 266 console.log(`[verbose] SHA256 of signed op: ${Buffer.from(digest).toString('hex')}`); 267 } 268 269 writeFileSync(`${args.out}/genesis_unsigned.dag-cbor`, Buffer.from(unsignedCbor)); 270 writeFileSync(`${args.out}/genesis_signed.json`, `${JSON.stringify(signed)}\n`); 271 writeFileSync(`${args.out}/did.txt`, `${did}\n`); 272 writeFileSync(`${args.out}/rotation_did_key.txt`, `${kb.didKey}\n`); 273 274 if (args.verbose) { 275 console.log(`[verbose] Wrote genesis_unsigned.dag-cbor (${unsignedCbor.length} bytes)`); 276 console.log('[verbose] Wrote genesis_signed.json'); 277 console.log('[verbose] Wrote did.txt'); 278 console.log('[verbose] Wrote rotation_did_key.txt'); 279 } 280 281 console.log(`Rotation key (did:key): ${kb.didKey}`); 282 console.log(`DID (derived): ${did}`); 283 console.log(`Wrote keys & artifacts to: ${resolve(args.out)}`); 284 285 if (args.dryRun) { 286 console.log('Dry run selected; not POSTing to PLC.'); 287 return; 288 } 289 290 const url = `https://plc.directory/${did}`; 291 if (args.verbose) { 292 console.log(`[verbose] POSTing to ${url}`); 293 console.log('[verbose] Request body:'); 294 console.log(JSON.stringify(signed, null, 2)); 295 } 296 297 try { 298 const resp = await fetch(url, { 299 method: 'POST', 300 headers: { 'Content-Type': 'application/json' }, 301 body: JSON.stringify(signed), 302 signal: AbortSignal.timeout(10000), 303 }); 304 const text = await resp.text(); 305 console.log(`POST ${url} -> ${resp.status}`); 306 console.log(text.slice(0, 5000)); 307 if (resp.ok) { 308 console.log('Registration appears successful.'); 309 } else { 310 console.log('Registration failed; see response above.'); 311 } 312 } catch (e) { 313 process.stderr.write(`Error POSTing to PLC: ${e}\n`); 314 process.exit(2); 315 } 316} 317 318await main();