open source is social v-it.org
0
fork

Configure Feed

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

Add JavaScript/Bun port of plc_register.py

Add a single-file Bun CLI with feature parity to the Python implementation: k256/p256 key generation, DAG-CBOR signing, low-S canonicalization, DID derivation, PEM export, dry-run mode, and PLC directory registration.

Add JavaScript dependencies: @noble/curves, @noble/hashes, @ipld/dag-cbor, bs58, and commander.

+332
+3
.gitignore
··· 31 31 32 32 # PLC keys and artifacts 33 33 plc_keys/ 34 + node_modules/ 35 + bun.lockb 36 + bun.lock
+11
package.json
··· 1 + { 2 + "name": "vit", 3 + "type": "module", 4 + "dependencies": { 5 + "@noble/curves": "^1.8.0", 6 + "@noble/hashes": "^1.7.0", 7 + "@ipld/dag-cbor": "^9.2.0", 8 + "bs58": "^6.0.0", 9 + "commander": "^13.0.0" 10 + } 11 + }
+318
plc_register.js
··· 1 + #!/usr/bin/env bun 2 + 3 + import { secp256k1 } from '@noble/curves/secp256k1'; 4 + import { p256 } from '@noble/curves/p256'; 5 + import { sha256 } from '@noble/hashes/sha256'; 6 + import { encode as dagCborEncode } from '@ipld/dag-cbor'; 7 + import bs58 from 'bs58'; 8 + import { Command } from 'commander'; 9 + import { mkdirSync, writeFileSync } from 'node:fs'; 10 + import { resolve } from 'node:path'; 11 + 12 + const MC_P256_PUB = new Uint8Array([0x80, 0x24]); 13 + const MC_K256_PUB = new Uint8Array([0xe7, 0x01]); 14 + const N_K256 = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; 15 + const N_P256 = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n; 16 + 17 + const 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 + 22 + const 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 + 27 + function ensureDir(path) { 28 + mkdirSync(path, { recursive: true }); 29 + } 30 + 31 + function b64urlNopad(data) { 32 + return Buffer.from(data).toString('base64url').replace(/=+$/g, ''); 33 + } 34 + 35 + function 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 + 54 + function 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 + 64 + function 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 + 72 + function 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 + 80 + function 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 + 92 + function 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 + 104 + function 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 + 114 + function derWrap(tag, content) { 115 + const body = Buffer.from(content); 116 + return Buffer.concat([Buffer.from([tag]), derLength(body.length), body]); 117 + } 118 + 119 + function derSequence(parts) { 120 + const content = Buffer.concat(parts.map((part) => Buffer.from(part))); 121 + return derWrap(0x30, content); 122 + } 123 + 124 + function 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 + 130 + function derOctetString(data) { 131 + return derWrap(0x04, data); 132 + } 133 + 134 + function derBitString(data) { 135 + return derWrap(0x03, Buffer.concat([Buffer.from([0x00]), Buffer.from(data)])); 136 + } 137 + 138 + function 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 + 148 + function buildSpkiDer(curve, uncompressedPubkey) { 149 + const { algId } = getCurve(curve); 150 + return derSequence([algId, derBitString(uncompressedPubkey)]); 151 + } 152 + 153 + function 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 + 159 + function 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 + 192 + function 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 + 211 + function 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 + 218 + function collect(value, previous) { 219 + previous.push(value); 220 + return previous; 221 + } 222 + 223 + async 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 + 318 + await main();