this repo has no description

feat: add PLC operation signing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+183 -2
scripts
+183 -2
scripts/setup.js
··· 148 148 return result 149 149 } 150 150 151 + // === CBOR ENCODING (minimal for PLC operations) === 152 + 153 + function cborEncode(value) { 154 + const parts = [] 155 + 156 + function encode(val) { 157 + if (val === null) { 158 + parts.push(0xf6) 159 + } else if (typeof val === 'string') { 160 + const bytes = new TextEncoder().encode(val) 161 + encodeHead(3, bytes.length) 162 + parts.push(...bytes) 163 + } else if (typeof val === 'number') { 164 + if (Number.isInteger(val) && val >= 0) { 165 + encodeHead(0, val) 166 + } 167 + } else if (val instanceof Uint8Array) { 168 + encodeHead(2, val.length) 169 + parts.push(...val) 170 + } else if (Array.isArray(val)) { 171 + encodeHead(4, val.length) 172 + for (const item of val) encode(item) 173 + } else if (typeof val === 'object') { 174 + const keys = Object.keys(val).sort() 175 + encodeHead(5, keys.length) 176 + for (const key of keys) { 177 + encode(key) 178 + encode(val[key]) 179 + } 180 + } 181 + } 182 + 183 + function encodeHead(majorType, length) { 184 + const mt = majorType << 5 185 + if (length < 24) { 186 + parts.push(mt | length) 187 + } else if (length < 256) { 188 + parts.push(mt | 24, length) 189 + } else if (length < 65536) { 190 + parts.push(mt | 25, length >> 8, length & 0xff) 191 + } 192 + } 193 + 194 + encode(value) 195 + return new Uint8Array(parts) 196 + } 197 + 198 + // === HASHING === 199 + 200 + async function sha256(data) { 201 + const hash = await webcrypto.subtle.digest('SHA-256', data) 202 + return new Uint8Array(hash) 203 + } 204 + 205 + // === PLC OPERATIONS === 206 + 207 + async function signPlcOperation(operation, privateKey) { 208 + // Encode operation without sig field 209 + const { sig, ...opWithoutSig } = operation 210 + const encoded = cborEncode(opWithoutSig) 211 + 212 + // Sign with P-256 213 + const signature = await webcrypto.subtle.sign( 214 + { name: 'ECDSA', hash: 'SHA-256' }, 215 + privateKey, 216 + encoded 217 + ) 218 + 219 + // Convert to low-S form and base64url encode 220 + const sigBytes = ensureLowS(new Uint8Array(signature)) 221 + return base64UrlEncode(sigBytes) 222 + } 223 + 224 + function ensureLowS(sig) { 225 + // P-256 order N 226 + const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') 227 + const halfN = N / 2n 228 + 229 + const r = sig.slice(0, 32) 230 + const s = sig.slice(32, 64) 231 + 232 + // Convert s to BigInt 233 + let sInt = BigInt('0x' + bytesToHex(s)) 234 + 235 + // If s > N/2, replace with N - s 236 + if (sInt > halfN) { 237 + sInt = N - sInt 238 + const newS = hexToBytes(sInt.toString(16).padStart(64, '0')) 239 + const result = new Uint8Array(64) 240 + result.set(r) 241 + result.set(newS, 32) 242 + return result 243 + } 244 + 245 + return sig 246 + } 247 + 248 + function hexToBytes(hex) { 249 + const bytes = new Uint8Array(hex.length / 2) 250 + for (let i = 0; i < hex.length; i += 2) { 251 + bytes[i / 2] = parseInt(hex.substr(i, 2), 16) 252 + } 253 + return bytes 254 + } 255 + 256 + function base64UrlEncode(bytes) { 257 + const binary = String.fromCharCode(...bytes) 258 + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 259 + } 260 + 261 + async function createGenesisOperation(opts) { 262 + const { didKey, handle, pdsUrl, cryptoKey } = opts 263 + 264 + // Build the full handle 265 + const pdsHost = new URL(pdsUrl).host 266 + const fullHandle = `${handle}.${pdsHost}` 267 + 268 + const operation = { 269 + type: 'plc_operation', 270 + rotationKeys: [didKey], 271 + verificationMethods: { 272 + atproto: didKey 273 + }, 274 + alsoKnownAs: [`at://${fullHandle}`], 275 + services: { 276 + atproto_pds: { 277 + type: 'AtprotoPersonalDataServer', 278 + endpoint: pdsUrl 279 + } 280 + }, 281 + prev: null 282 + } 283 + 284 + // Sign the operation 285 + operation.sig = await signPlcOperation(operation, cryptoKey) 286 + 287 + return { operation, fullHandle } 288 + } 289 + 290 + async function deriveDidFromOperation(operation) { 291 + const { sig, ...opWithoutSig } = operation 292 + const encoded = cborEncode(opWithoutSig) 293 + const hash = await sha256(encoded) 294 + // DID is base32 of first 24 bytes of hash 295 + return 'did:plc:' + base32Encode(hash.slice(0, 24)) 296 + } 297 + 298 + function base32Encode(bytes) { 299 + const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' 300 + let result = '' 301 + let bits = 0 302 + let value = 0 303 + 304 + for (const byte of bytes) { 305 + value = (value << 8) | byte 306 + bits += 8 307 + while (bits >= 5) { 308 + bits -= 5 309 + result += alphabet[(value >> bits) & 31] 310 + } 311 + } 312 + 313 + if (bits > 0) { 314 + result += alphabet[(value << (5 - bits)) & 31] 315 + } 316 + 317 + return result 318 + } 319 + 151 320 // === MAIN === 152 321 153 322 async function main() { ··· 164 333 const keyPair = await generateP256Keypair() 165 334 const didKey = publicKeyToDidKey(keyPair.publicKey) 166 335 console.log(` did:key: ${didKey}`) 167 - console.log(` Private key: ${bytesToHex(keyPair.privateKey)}`) 336 + console.log('') 337 + 338 + // Step 2: Create genesis operation 339 + console.log('Creating PLC genesis operation...') 340 + const { operation, fullHandle } = await createGenesisOperation({ 341 + didKey, 342 + handle: opts.handle, 343 + pdsUrl: opts.pds, 344 + cryptoKey: keyPair.cryptoKey 345 + }) 346 + const did = await deriveDidFromOperation(operation) 347 + console.log(` DID: ${did}`) 348 + console.log(` Handle: ${fullHandle}`) 168 349 console.log('') 169 350 170 - // TODO: Register DID:PLC 351 + // TODO: Register with plc.directory 171 352 // TODO: Initialize PDS 172 353 // TODO: Notify relay 173 354 }