open source is social v-it.org
0

Configure Feed

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

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