OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-room / lib / membership.js

54 lines · javascript ref:
// Membership — binary attestation admission for the swarm.
//
// One member type: the N devices of ONE DID, all equal. A device is admitted iff it can prove it
// is attested to the DID's ROOT identity (a keet-identity-key chain: root → device). No roles.
// These functions plug into StateBaseV3's injectable admission (opts.attest / opts.verifyAttest):
// the device attests the autobase's admission bytes (localKey‖base.key) as itself, and any writer
// re-verifies that the proof resolves to the DID's root identity + the claimed device key.
//
// The device roster (opts.getMembers) is the set of root-attested devices — supplied by the host
// (all role-equal); admission is proof-gated, the roster only says "these device ids are members".

import IdentityKey from 'keet-identity-key'
import keetEncoding from 'keet-identity-key/lib/encoding.js'
import c from 'compact-encoding'
import b4a from 'b4a'
import { attestDeviceChain, attestDeviceViaDevice, deviceKeetKeyPair, rootIdentityPublicKey } from 'atproto-identity/lib/device-chain.js'

const { ReceiptEncoding } = keetEncoding

// Epoch enforcement (atproto-base-REVOCATION §3): a receipt encodes a MINIMUM acceptable attestation
// epoch. Admission passes the current __receipt__ to verify, which rejects any proof whose epoch is
// below it. Bump it (encodeReceipt of a higher epoch, + re-attest survivors) to invalidate ALL stale
// proofs at once — the keet verifyAttestation(…, latestReceipt) mechanism.
export function encodeReceipt (epoch) { return c.encode(ReceiptEncoding, { epoch: Number(epoch) }) }
export function receiptEpoch (receipt) { return c.decode(ReceiptEncoding, b4a.isBuffer(receipt) ? receipt : b4a.from(receipt)).epoch }

// attest fn for StateBaseV3: sign arbitrary bytes as this device, carrying its root→device chain.
// deviceKeetKeyPair = this device's Ed25519 identity keypair; deviceProof = the root's attestation
// of this device (from attestDeviceChain / keet-identity-key bootstrap).
export function makeAttest (deviceKeetKeyPair, deviceProof) {
  return async (signData) => {
    const proof = IdentityKey.attestData(b4a.from(signData), deviceKeetKeyPair, deviceProof)
    return b4a.isBuffer(proof) ? proof : b4a.from(proof) // StateBaseV3 hex-encodes it into the admit
  }
}

// verifyAttest fn for StateBaseV3, bound to the DID's root identity pubkey. Returns bool: the
// proof over signData must resolve to (root identity, claimed device id).
export function makeVerifyAttest (rootIdentityPublicKey) {
  const root = b4a.from(rootIdentityPublicKey)
  // Returns the verify RESULT ({ receipt, identityPublicKey, devicePublicKey }, truthy) on success,
  // or null. `receipt` (optional) is the current MINIMUM-epoch receipt — a proof below it is rejected.
  return async (proof, signData, deviceId, receipt) => {
    try {
      const opts = { expectedIdentity: root, expectedDevice: b4a.from(deviceId) }
      if (receipt) opts.receipt = b4a.isBuffer(receipt) ? receipt : b4a.from(receipt)
      const r = IdentityKey.verify(b4a.from(proof), b4a.from(signData), opts)
      return r || null
    } catch { return null }
  }
}

export { attestDeviceChain, attestDeviceViaDevice, deviceKeetKeyPair, rootIdentityPublicKey }