chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The identity half of setup: a secp256k1 signing key, its PLC operation and
// the DID derived from it. Everything here runs in the visitor's browser. The
// key is secp256k1 rather than P-256 because some third-party services verify
// only ES256K service JWTs.
import { p256 } from '@noble/curves/p256';
import { secp256k1 } from '@noble/curves/secp256k1';
import { base32Encode, bytesToHex } from '@pdsjs/core/crypto';
import {
buildPlcOperation,
publicKeyToDidKey,
signPlcOperationObject,
} from '@pdsjs/core/plc';
import { cborEncodeDagCbor } from '@pdsjs/core/repo';
export function generateIdentity() {
const privateKey = secp256k1.utils.randomPrivateKey();
const publicKey = secp256k1.getPublicKey(privateKey, true);
// The `#atproto_kem` key. Space syncers derive a shared secret against it.
const kemPrivateKey = p256.utils.randomPrivateKey();
return {
privateKey,
privateKeyHex: bytesToHex(privateKey),
didKey: publicKeyToDidKey(publicKey, 'secp256k1'),
kemKeyHex: bytesToHex(kemPrivateKey),
kemDidKey: publicKeyToDidKey(
p256.getPublicKey(kemPrivateKey, true),
'p256',
),
/** @type {import('@pdsjs/core/crypto').SigningKey} */
signer: {
curve: 'secp256k1',
async sign(/** @type {Uint8Array} */ data) {
const hash = new Uint8Array(
// SigningKey takes any Uint8Array; SubtleCrypto takes one backed by
// an ArrayBuffer. Every caller here passes the latter.
await crypto.subtle.digest(
'SHA-256',
/** @type {Uint8Array<ArrayBuffer>} */ (data),
),
);
// Low-S compact r||s, the shape atproto verifies.
return secp256k1
.sign(hash, privateKey, { lowS: true })
.toCompactRawBytes();
},
async publicKey() {
return publicKey;
},
},
};
}
/**
* @param {ReturnType<typeof generateIdentity>} identity
* @param {string} handle
* @param {string} pdsUrl
*/
export async function createPlcOperation(identity, handle, pdsUrl) {
const operation = await signPlcOperationObject(
buildPlcOperation({
didKey: identity.didKey,
kemDidKey: identity.kemDidKey,
handle,
pdsEndpoint: pdsUrl,
}),
identity.signer,
);
// The DID covers the signature, so it is derived from the signed operation.
const digest = new Uint8Array(
await crypto.subtle.digest(
'SHA-256',
/** @type {Uint8Array<ArrayBuffer>} */ (cborEncodeDagCbor(operation)),
),
);
const did = `did:plc:${base32Encode(digest.slice(0, 15))}`;
return { operation, did };
}