Something went wrong. Try again.
A Space host for truly public data
Something went wrong. Try again.
6.2 kB · 172 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173import { Secp256k1Keypair } from '@atproto/crypto'import { type DidDocument } from '@atproto/identity'import { type SpaceHostDb } from './db'
// Published-key lookups are cached: generously when a key is found, briefly// when not, so a fresh PLC operation is picked up quickly.const DOC_CACHE_MS = 5 * 60_000const DOC_NEGATIVE_CACHE_MS = 30_000
export type KeyPoolOpts = { /** Resolve a DID document. Injectable for tests. */ resolveDidDoc: (did: string) => Promise<DidDocument | null> /** Sliding lifetime for pooled keys. */ keyTtlMs: number /** * Dev mode: skip DID documents entirely and sign with an ephemeral * per-DID key. Never use in production. */ trustWithoutDoc?: boolean /** Cache TTL overrides, for tests. */ docCacheMs?: number docNegativeCacheMs?: number}
export type Signer = | { keypair: Secp256k1Keypair; reason?: undefined } | { keypair: null; reason: string }
export type AuthorityStatus = { did: string /** The #atproto_space key the DID document currently publishes, if any. */ publishedKey: string | null /** Whether the pool holds the matching private key. */ holdsKey: boolean /** publishedKey present AND held: this host can sign for the DID. */ canSign: boolean reason?: string}
/** * An anonymous pool of signing keys. The host stores no DID→key binding: * the DID document is the only binding. At credential time the authority's * document is resolved, its published #atproto_space key looked up in the * pool, and the matching private key signs — no match, no signing. * * Anyone may add a key (rate-limited upstream). An unreferenced key is * harmless — it can only ever sign for a DID whose owner publishes it — and * expires if unused. Rotation needs no ceremony: add the new key to the * pool (before or after updating the DID document) and the host follows * whatever the document says. */export class KeyPool { private keypairCache = new Map<string, Secp256k1Keypair>() private docKeyCache = new Map<string, { multibase: string | null; expiresAt: number }>() private devKeys = new Map<string, Secp256k1Keypair>()
constructor( private db: SpaceHostDb, private opts: KeyPoolOpts, ) {}
/** Add (or refresh) a key in the pool. Generates one when none is supplied. */ async add( suppliedKeyHex?: string, ): Promise<{ keypair: Secp256k1Keypair; expiresAt: string; alreadyHeld: boolean }> { const keypair = suppliedKeyHex ? await Secp256k1Keypair.import(suppliedKeyHex.trim(), { exportable: true }) : await Secp256k1Keypair.create({ exportable: true }) const publicKey = multikey(keypair) const alreadyHeld = this.db.getSigningKey(publicKey) !== null const expiresAt = new Date(Date.now() + this.opts.keyTtlMs).toISOString() const hex = Buffer.from(await keypair.export()).toString('hex') this.db.addSigningKey(publicKey, hex, expiresAt) this.keypairCache.set(publicKey, keypair) return { keypair, expiresAt, alreadyHeld } }
/** * The signing keypair for an authority, resolved through its DID * document. Using a key refreshes its expiry, so keys in active use * never age out. */ async signerFor(did: string): Promise<Signer> { if (this.opts.trustWithoutDoc) { let keypair = this.devKeys.get(did) if (!keypair) { keypair = await Secp256k1Keypair.create() this.devKeys.set(did, keypair) } return { keypair } } const publishedKey = await this.publishedSpaceKey(did) if (!publishedKey) { return { keypair: null, reason: `the DID document for ${did} does not resolve or publishes no #atproto_space key`, } } const row = this.db.getSigningKey(publishedKey) if (!row) { return { keypair: null, reason: `this host does not hold the private key for ${did}'s published #atproto_space key (${publishedKey}) — add it via POST /keys`, } } let keypair = this.keypairCache.get(publishedKey) if (!keypair) { keypair = await Secp256k1Keypair.import(row.privateKeyHex, { exportable: true }) this.keypairCache.set(publishedKey, keypair) } this.db.touchSigningKey( publishedKey, new Date(Date.now() + this.opts.keyTtlMs).toISOString(), ) return { keypair } }
/** The did:key to verify an authority's credentials against, if known. */ async verifierFor(did: string): Promise<string | null> { if (this.opts.trustWithoutDoc) { return this.devKeys.get(did)?.did() ?? null } const publishedKey = await this.publishedSpaceKey(did) return publishedKey ? `did:key:${publishedKey}` : null }
async status(did: string): Promise<AuthorityStatus> { const signer = await this.signerFor(did) if (this.opts.trustWithoutDoc) { return { did, publishedKey: null, holdsKey: true, canSign: true } } const publishedKey = await this.publishedSpaceKey(did) return { did, publishedKey, holdsKey: signer.keypair !== null, canSign: signer.keypair !== null, reason: signer.reason, } }
/** The #atproto_space public key (multibase) from the DID document, cached. */ private async publishedSpaceKey(did: string): Promise<string | null> { const cached = this.docKeyCache.get(did) if (cached && cached.expiresAt > Date.now()) { return cached.multibase } let multibase: string | null = null try { const doc = await this.opts.resolveDidDoc(did) if (doc) multibase = docSpaceKey(doc, did) ?? null } catch { // Resolution failure behaves like an unpublished key, briefly cached. } const ttl = multibase ? (this.opts.docCacheMs ?? DOC_CACHE_MS) : (this.opts.docNegativeCacheMs ?? DOC_NEGATIVE_CACHE_MS) this.docKeyCache.set(did, { multibase, expiresAt: Date.now() + ttl }) return multibase }}
export function multikey(keypair: Secp256k1Keypair): string { return keypair.did().replace('did:key:', '')}
function docSpaceKey(doc: DidDocument, did: string): string | undefined { const method = doc.verificationMethod?.find( (m) => m.id === '#atproto_space' || m.id === `${did}#atproto_space`, ) return method?.publicKeyMultibase}