OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-identity / lib / device-attestation.js

77 lines · javascript ref:
// 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)
}