OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-bridge / pds / test / bare / fronting.test.js

174 lines · javascript ref:
// Asking this membrane to carry an identity, with no operator in the loop.
//
// ⛔ WHAT WAS MISSING. allowFront() only un-denies; a DID enters the fronted set through add(),
// reachable only from config at boot or from the owner-gated control socket. So a peer holding a
// perfectly good identity had no way to ask to be carried — the same gap ap-bridge had for names.
//
// ⚠ The proof here is NOT what protects the identity: this membrane is keyless and refuses any
// commit that does not verify against the PLC-resolved key, so naming someone else's identity would
// only make it serve their real, signed repo. The proof bounds ABUSE — carrying an identity costs a
// topic join and replication for ever.

import test from 'brittle'
import b4a from 'b4a'
import IdentityKey from 'keet-identity-key'
import crypto from 'hypercore-crypto'
import { signAttested } from 'favela-identity/lib/attestation.js'
import { Admission } from 'favela-identity/lib/admission.js'
import { frontHandlers } from '../../lib/fronting.js'

const ID = 'did:plc:abcdefghijklmnopqrstuvwx'

async function identity () {
  const idk = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
  const kp = crypto.keyPair()
  const proof = await IdentityKey.bootstrap({ identity: idk.identityKeyPair }, kp.publicKey)
  return { rootHex: b4a.toString(idk.identityKeyPair.publicKey, 'hex'), cred: { keyPair: kp, proof: b4a.toString(proof, 'base64') } }
}

function fakePeers () {
  const added = []
  return { added, add: (id) => { added.push(id) }, isFronted: (id) => added.includes(id), resolveHandle: () => null }
}

const deps = (extra = {}) => ({ peers: fakePeers(), admission: new Admission(), ...extra })

test('★★ a peer asks to be carried, and is', async (t) => {
  const d = deps()
  const h = frontHandlers(d)
  const me = await identity()

  const { nonce } = await h.challenge()
  const proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), me.cred)
  const r = await h.front({ id: ID, root: me.rootHex, nonce, proof })

  t.is(r.status, 200)
  t.ok(r.body.fronted)
  t.alike(d.peers.added, [ID], 'and the router was told to carry it')
})

test('⛔ an unproven request carries nothing', async (t) => {
  const d = deps()
  const h = frontHandlers(d)
  const me = await identity()

  t.is((await h.front({ id: ID, root: me.rootHex })).status, 403, 'no challenge, no proof')
  t.alike(d.peers.added, [], 'nothing was fronted')

  const { nonce } = await h.challenge()
  const other = await identity()
  const proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), other.cred)
  const r = await h.front({ id: ID, root: me.rootHex, nonce, proof })
  t.is(r.status, 403, 'a proof from somebody else')
  t.is(r.body.error, 'proof does not attest that identity')
  t.alike(d.peers.added, [])
})

test('⛔ a proof cannot be replayed for a different identity', async (t) => {
  const d = deps()
  const h = frontHandlers(d)
  const me = await identity()
  const { nonce } = await h.challenge()
  const proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), me.cred)

  t.is((await h.front({ id: 'did:plc:somebodyelseentirely00', root: me.rootHex, nonce, proof })).status, 403)
  t.alike(d.peers.added, [])
})

// ⚠ A peer that retried after a dropped connection must not be told it did something wrong.
test('★ asking twice is not an error', async (t) => {
  const d = deps()
  const h = frontHandlers(d)
  const me = await identity()

  for (let i = 0; i < 2; i++) {
    const { nonce } = await h.challenge()
    const proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), me.cred)
    const r = await h.front({ id: ID, root: me.rootHex, nonce, proof })
    t.is(r.status, 200)
    if (i === 1) t.ok(r.body.already, 'and it says it was already carried')
  }
})

// ⛔ frontedDids is CONFIG; this is not. A restart that silently unfronted everyone who asked would
// make the whole door a lie.
test('⛔ what a peer asked for is persisted, and it says so when it cannot be', async (t) => {
  const saved = []
  const d = deps({ onFronted: async (x) => { saved.push(x) } })
  const h = frontHandlers(d)
  const me = await identity()

  let { nonce } = await h.challenge()
  let proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), me.cred)
  let r = await h.front({ id: ID, root: me.rootHex, nonce, proof })
  t.ok(r.body.durable)
  t.is(saved.length, 1)
  t.is(saved[0].root, me.rootHex, 'and WHO asked is recorded, not only what')

  const d2 = deps({ onFronted: async () => { throw new Error('disk full') } })
  const h2 = frontHandlers(d2)
  ;({ nonce } = await h2.challenge())
  proof = signAttested(d2.admission.frontBytes({ nonce, id: ID }), me.cred)
  r = await h2.front({ id: ID, root: me.rootHex, nonce, proof })
  t.is(r.status, 202, 'carried, and honest that it will not survive a restart')
  t.absent(r.body.durable)
  t.ok(d2.peers.isFronted(ID), 'it IS fronted — sending the peer away would be the wrong lie')
})

test('⛔ a bridge that does not take requests says so', async (t) => {
  const d = deps()
  const h = frontHandlers({ ...d, policy: 'closed' })
  const me = await identity()
  const { nonce } = await h.challenge()
  const proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), me.cred)
  t.is((await h.front({ id: ID, root: me.rootHex, nonce, proof })).status, 403)
  t.alike(d.peers.added, [])
})

// ⛔ THE MEMBRANE GRANTS THE HANDLE, and hands back what the peer must put on-chain next. hpp's mint
// names this step: the handle and service endpoint "arrive LATER, as a PLC update op, when an invite
// from a bridge is accepted". Without these two values coming back, the peer has nothing to adopt.
test('★★ fronting grants a handle and names the endpoint to adopt', async (t) => {
  const saved = []
  const d = deps({ userDomain: '.7rnx.net', pdsUrl: 'https://pds.7rnx.net', onFronted: async (x) => saved.push(x) })
  const h = frontHandlers(d)
  const me = await identity()

  const offer = await h.policy()
  t.is(offer.userDomain, '.7rnx.net', 'a peer can see the suffix BEFORE committing to a permanent append')
  t.is(offer.pdsUrl, 'https://pds.7rnx.net')

  const { nonce } = await h.challenge()
  const proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), me.cred)
  const r = await h.front({ id: ID, name: 'alice', root: me.rootHex, nonce, proof })

  t.is(r.status, 200)
  t.is(r.body.handle, 'alice.7rnx.net', 'granted under the membrane domain, not chosen by the peer')
  t.is(r.body.pdsUrl, 'https://pds.7rnx.net', 'and the endpoint the DID must point at')
  t.is(saved[0].handle, 'alice.7rnx.net', 'persisted, or a restart forgets the name it granted')
})

test('⛔ a handle held by somebody else is refused, not reassigned', async (t) => {
  const d = deps({ userDomain: '.7rnx.net' })
  d.peers.resolveHandle = (h) => (h === 'alice.7rnx.net' ? 'did:plc:somebodyelse00000000000' : null)
  const h = frontHandlers(d)
  const me = await identity()
  const { nonce } = await h.challenge()
  const proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), me.cred)

  const r = await h.front({ id: ID, name: 'alice', root: me.rootHex, nonce, proof })
  t.is(r.status, 409)
  t.is(r.body.error, 'taken')
  t.alike(d.peers.added, [], 'and nothing was fronted')
})

test('⛔ a garbage handle is refused before anything is recorded', async (t) => {
  const d = deps({ userDomain: '.7rnx.net' })
  const h = frontHandlers(d)
  const me = await identity()
  const { nonce } = await h.challenge()
  const proof = signAttested(d.admission.frontBytes({ nonce, id: ID }), me.cred)
  t.is((await h.front({ id: ID, name: 'not a handle', root: me.rootHex, nonce, proof })).status, 400)
  t.alike(d.peers.added, [])
})