// Device attestation — the User owns the feed, and the roster is gated by proof.
//
// ── The rule ──────────────────────────────────────────────────────────────────────────────────
//
// A feed's genesis BINDS an identity root. After that, the only way onto the roster is a chain
// proof that resolves to that root. Nobody vouches; nobody needs to be online. A device that can
// prove it descends from the feed's owner admits ITSELF.
//
// That deletes the old rule's awkwardness rather than adding to it: no "an admitted device must
// admit you", and no ambiguity about two devices each believing they are first — two devices under
// one root are both legitimately admitted, in whatever order linearizes.
//
// ── Why apply may do this at all ──────────────────────────────────────────────────────────────
//
// `IdentityKey.verify` is PURE and SYNCHRONOUS — measured under Bare: no Promise, ~76µs, no I/O.
// So it can run inside apply without breaking determinism. It THROWS on malformed input, which
// would wedge the base, so every call goes through `readProof`, which never throws.
//
// ── There is no unattested feed ───────────────────────────────────────────────────────────────
//
// Genesis REQUIRES a proof. A feed with no bound root cannot exist, so there is no second
// authorisation rule to remember and no mode anyone can be in by accident. The deployed feeds
// predate this and are discarded — favela-peer is days old and the owner said so explicitly.
import test from 'brittle'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import IdentityKey from 'keet-identity-key'
import { applyOp, ROSTER, REVOKED, FOUNDER } from '../lib/feed.js'
import { readProof } from '../lib/attestation.js'
// ⚠ The shared helper, not a local copy. This file had its own `identity()` that lacked
// `proofFor`, so a test calling it committed red — one definition, one behaviour.
import { identity } from './helpers.js'
const kv = () => {
const m = new Map()
return {
m,
async set (k, v) { m.set(k, v) },
async get (k) { return m.has(k) ? m.get(k) : null }
}
}
const hex = (k) => b4a.toString(k, 'hex')
const run = async (ops, view = kv()) => {
for (const [device, op] of ops) await applyOp(view, op, { device })
return view
}
const roster = (v, writer) => v.get(ROSTER + '/' + writer)
const revoked = (v, dev) => v.get(REVOKED + '/' + dev)
// A writer core key. Opaque, manifest-derived in production, never able to sign — which is the
// whole reason it is bound to a device as data rather than being the attested key itself.
const writer = () => hex(crypto.randomBytes(32))
const add = (writerKey, proof) => ({ t: 'add-device', device: writerKey, proof })
const remove = (device) => ({ t: 'remove-device', device })
// ── genesis ────────────────────────────────────────────────────────────────────────────────────
test('genesis BINDS the identity root, and the roster records the DEVICE', async (t) => {
const id = await identity()
const d = await id.device()
const w = writer()
const v = await run([[w, add(w, d.bind(w))]])
t.alike(await roster(v, w), { device: d.hex, admittedBy: w },
'the roster maps a writer core to the device that vouches for it')
t.alike(await v.get(FOUNDER), { device: w, identityRoot: id.rootHex },
'and genesis binds the owner, once, permanently')
})
test('⛔ genesis without a proof is REFUSED — there is no unattested feed', async (t) => {
const w1 = writer()
const w2 = writer()
const v = await run([[w1, add(w1)], [w1, add(w2)]])
t.is(await v.get(FOUNDER), null, 'no founder, so no feed')
t.absent(await roster(v, w1), 'and nothing was admitted')
})
// ── admission ──────────────────────────────────────────────────────────────────────────────────
test('★ an attested device admits ITSELF — nobody vouches, nobody is online', async (t) => {
const id = await identity()
const a = await id.device()
const b = await id.device()
const wa = writer()
const wb = writer()
const v = await run([[wa, add(wa, a.bind(wa))], [wb, add(wb, b.bind(wb))]])
t.ok(await roster(v, wb), 'a proof to the bound root is the whole authorisation')
})
test('★ ONE device can hold several writer cores', async (t) => {
const id = await identity()
const a = await id.device()
const w1 = writer()
const w2 = writer()
const v = await run([[w1, add(w1, a.bind(w1))], [w1, add(w2, a.bind(w2))]])
t.is((await roster(v, w1)).device, a.hex)
t.is((await roster(v, w2)).device, a.hex, 'the device is the identity; a writer core is not')
})
test('⛔ a device under a DIFFERENT root is refused', async (t) => {
const mine = await identity()
const theirs = await identity()
const a = await mine.device()
const x = await theirs.device()
const wa = writer()
const wx = writer()
const v = await run([[wa, add(wa, a.bind(wa))], [wx, add(wx, x.bind(wx))]])
t.absent(await roster(v, wx), 'their proof is valid — for their identity, not mine')
})
test('⛔ a proof is bound to ONE writer key and cannot be replayed onto another', async (t) => {
const id = await identity()
const a = await id.device()
const wa = writer()
const mine = writer()
const other = writer()
// The proof attests `mine` as data. Presenting it for `other` must fail.
const v = await run([[wa, add(wa, a.bind(wa))], [wa, add(other, a.bind(mine))]])
t.absent(await roster(v, other), 'attested data is what stops a proof being a bearer token')
})
test('⛔ apply is TOTAL — malformed proofs are refused, never thrown', async (t) => {
const id = await identity()
const a = await id.device()
const wa = writer()
const v = kv()
await run([[wa, add(wa, a.bind(wa))]], v)
const junk = [
'', 'not base64 at all!!', b4a.toString(b4a.alloc(0), 'base64'),
b4a.toString(crypto.randomBytes(64), 'base64'),
b4a.toString(b4a.from(a.proof, 'base64').subarray(0, 10), 'base64'),
a.proof // ⛔ a valid DEVICE proof that carries no attested data — this one throws in blake2b
]
for (const bad of junk) {
const w = writer()
await applyOp(v, add(w, bad), { device: w })
t.absent(await roster(v, w), 'refused: ' + JSON.stringify(String(bad).slice(0, 16)))
}
})
// ── linking a second machine, with no mnemonic ─────────────────────────────────────────────────
test('★★ a linked device attests the NEXT one — this is the blind-pairing path', async (t) => {
const id = await identity()
const a = await id.device()
const b = await a.child() // attested by A, from A's own keypair. No vault, no mnemonic.
const wa = writer()
const wb = writer()
const v = await run([[wa, add(wa, a.bind(wa))], [wb, add(wb, b.bind(wb))]])
t.ok(await roster(v, wb), 'B is admitted, and the vault was never opened')
t.is((await roster(v, wb)).device, b.hex)
})
// ── revocation ─────────────────────────────────────────────────────────────────────────────────
test('revocation targets the DEVICE, and un-admits every core it holds', async (t) => {
const id = await identity()
const a = await id.device()
const b = await id.device()
const wa = writer()
const wb1 = writer()
const wb2 = writer()
const v = await run([
[wa, add(wa, a.bind(wa))],
[wb1, add(wb1, b.bind(wb1))],
[wb2, add(wb2, b.bind(wb2))],
[wa, remove(b.hex)]
])
t.ok(await revoked(v, b.hex), 'positive evidence, not an absence')
// Both of B's cores stop counting at once — no roster walk needed.
for (const w of [wb1, wb2]) {
await applyOp(v, { t: 'put', collection: 'post', name: 'p' + w.slice(0, 4), $oid: 'o', size: 1 }, { device: w })
}
const posts = [...v.m.keys()].filter((k) => k.startsWith('post/'))
t.alike(posts, [], 'a revoked device cannot write, through any core it holds')
})
test('⛔ THE HOLE: a revoked device cannot re-admit itself with its still-valid proof', async (t) => {
const id = await identity()
const a = await id.device()
const b = await id.device()
const wa = writer()
const wb = writer()
const v = await run([
[wa, add(wa, a.bind(wa))],
[wb, add(wb, b.bind(wb))],
[wa, remove(b.hex)],
[wb, add(wb, b.bind(wb))] // same device, same proof, still cryptographically valid
])
t.ok(await revoked(v, b.hex), 'the revocation stands')
// The assertion that matters is the CONSEQUENCE, not the bookkeeping: the re-admitted writer
// must still be unable to write. Asserting the roster row alone would pass trivially — the row
// from the first admission is still there, and it is the revocation list that overrides it.
await applyOp(v, { t: 'put', collection: 'post', name: 'after', $oid: 'o', size: 1 }, { device: wb })
t.is(await v.get('post/after'), null, 'a re-admitted revoked device still cannot write')
})
test('⛔ revoking a device reaches the devices it ATTESTED', async (t) => {
const id = await identity()
const a = await id.device()
const b = await id.device()
const c = await b.child() // B attested C — the escalation revocation must answer for
const wa = writer()
const wb = writer()
const wc = writer()
const v = await run([
[wa, add(wa, a.bind(wa))],
[wb, add(wb, b.bind(wb))],
[wa, remove(b.hex)],
[wc, add(wc, c.bind(wc))] // C's proof verifies cleanly and chains through the revoked B
])
t.absent(await roster(v, wc), 'a compromised device has descendants, so revocation must reach them')
})
test('revocation reaches descendants and nothing else', async (t) => {
const id = await identity()
const a = await id.device()
const b = await id.device()
const d = await id.device() // a sibling, attested by the root directly
const wa = writer()
const wb = writer()
const wd = writer()
const v = await run([
[wa, add(wa, a.bind(wa))],
[wb, add(wb, b.bind(wb))],
[wa, remove(b.hex)],
[wd, add(wd, d.bind(wd))]
])
t.ok(await roster(v, wd), 'a sibling is untouched — revocation is not a purge')
})
// ── the wrapper ────────────────────────────────────────────────────────────────────────────────
test('readProof binds a writer key and returns the device ancestry', async (t) => {
const id = await identity()
const a = await id.device()
const c = await a.child()
const w = writer()
const p = readProof(c.bind(w), w)
t.is(p.root, id.rootHex)
t.is(p.device, c.hex, 'the DEVICE key, not the writer key')
t.alike(p.chain, [a.hex, c.hex], 'ancestry in device keys — what revocation walks')
t.is(readProof(c.bind(w), writer()), null, 'a different writer key does not verify')
for (const bad of [null, undefined, '', 'zzz!!', b4a.alloc(0)]) {
t.is(readProof(bad, w), null, 'null, never a throw')
}
})