// Device-attested atproto anchoring.
//
// The atproto identity separates two roles (this is atproto's native design, and it maps
// onto a device chain — see docs/ATPROTO-P2P-BRIDGE.md §device-attestation):
//
// rotationKeys = authority / recovery keys (the ROOT identity, kept off the
// serving box). Any rotationKey can sign a PLC op to rotate the
// signing key, change the handle, or move the PDS.
// verificationMethod = the repo SIGNING key (a per-DEVICE key — the box that runs the
// PDS holds only this). Signs every commit.
//
// So the genesis op is signed by the ROOT rotation key and authorizes a DEVICE signing
// key. The device never holds the root seed; if the device is lost, the root rotates to a
// new device and the DID/handle/repos persist. did:plc = hash(genesis), so the rotation
// set is baked at genesis — this MUST be right before the one irreversible PLC write.
//
// This module owns the atproto (secp256k1 / PLC) side. The parallel keet-identity-key
// Ed25519 device chain (our P2P-verifiable attestation) lives in device-chain.js; the two
// run in parallel off the same device, bound by device-chain.js's attestData over the DID.
import { deriveAtprotoKey, keyFromPrivate, buildGenesisOp, signOp, verifyOp } from './atproto-identity.js'
import { unsignedOp, didForCreateOp } from './did-plc.js'
// Resolve a { privateKey, publicKey, didKey } from a mnemonic or a raw secp256k1 key.
function resolveKey (mnemonic, key, path) {
if (key) return keyFromPrivate(key)
if (mnemonic) return deriveAtprotoKey(mnemonic, path)
throw new Error('device-attestation: need a mnemonic or a key')
}
// Build a device-attested did:plc:
// rotationKeys = [root, ...extraRotationDidKeys] (root signs the genesis)
// verificationMethod = the device signing key
// Pass root by { rootMnemonic } or { rootKey } (secp256k1 private); device by
// { deviceMnemonic, devicePath } or { deviceKey }. Optionally list additional rotation
// did:keys (e.g. the device itself, or a second recovery device) for a real key chain.
export function buildAttestedIdentity ({
rootMnemonic, rootKey,
deviceMnemonic, deviceKey, deviceDidKey, devicePath,
handle, pdsUrl,
extraRotationDidKeys = []
}) {
const root = resolveKey(rootMnemonic, rootKey)
// Device: the REAL flow passes only the device's PUBLIC did:key — the device private key
// never leaves the device box, and the root just authorizes the pubkey into the genesis.
// The dev/test flow may pass a device mnemonic/key so the full signing key is available.
let deviceSigningKey = null
let deviceKeyId = deviceDidKey
if (!deviceKeyId) {
deviceSigningKey = resolveKey(deviceMnemonic, deviceKey, devicePath)
deviceKeyId = deviceSigningKey.didKey
}
const rotationDidKeys = [root.didKey, ...extraRotationDidKeys]
const unsigned = buildGenesisOp({ signingDidKey: deviceKeyId, rotationDidKeys, handle, pdsUrl })
const sig = signOp(unsigned, root.privateKey) // signed by the ROOT rotation key
const signedOp = { ...unsigned, sig }
const did = didForCreateOp(signedOp)
return {
did,
signedOp,
rootDidKey: root.didKey,
rootPublicKey: root.publicKey,
deviceDidKey: deviceKeyId,
deviceSigningKey, // { privateKey, publicKey, didKey } when available; null in the pubkey-only root flow
rotationKeys: rotationDidKeys
}
}
// Verify an attested genesis: it must be signed by (one of) its rotation keys — i.e. by
// the ROOT, not the device. Pass the root's secp256k1 public key.
export function verifyAttestedGenesis (signedOp, rootPublicKey) {
return verifyOp(unsignedOp(signedOp), signedOp.sig, rootPublicKey)
}