OverviewFilesCommitsIssuesDocsReleasesPeers

favela-identity / test / attestation.test.js

82 lines · javascript ref:
// The PRIMITIVE, on its own — what a verifier needs and nothing else.
//
// ⛔ favela-peer's `attest.test.js` stays where it is: it tests the FEED's roster apply, which is a
// different thing that happens to use this. What is asserted here is the property that lets a bridge
// admit somebody: verifying a chain proof is a pure function of the proof's bytes — no disk, no
// network, no clock — so a service can establish who you are while holding nothing of yours.
//
// ⛔ AND IT NEVER THROWS. `IdentityKey.verify` raises on malformed input (measured: an empty buffer
// and a truncated one both give "Out of bounds", random bytes give "uint is greater than the maximum
// safe integer"). This wrapper exists so a caller can treat a bad proof as a refusal rather than an
// exception — which matters most in exactly the place a hostile one arrives.

import test from 'brittle'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'
import IdentityKey from 'keet-identity-key'
import { readAttested, signAttested } from '../lib/attestation.js'

// ⚠ A DEVICE is a real Ed25519 keypair — one that can sign. `bootstrap` is what a vault does once,
// attesting the first device directly from the root.
async function identity () {
  const idk = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
  const root = idk.identityKeyPair
  const keyPair = crypto.keyPair()
  const proof = await IdentityKey.bootstrap({ identity: root }, keyPair.publicKey)
  return {
    rootHex: b4a.toString(root.publicKey, 'hex'),
    cred: { keyPair, proof: b4a.toString(proof, 'base64') }
  }
}

test('★★ a device proves it belongs to an identity, over arbitrary bytes', async (t) => {
  // This is the admission ceremony in miniature: a bridge hands out a challenge, the device signs
  // it, and the bridge learns the identity root without ever holding a key.
  const id = await identity()
  const challenge = b4a.from('ap-bridge:invite:9f2c1a', 'utf8')
  const signed = signAttested(challenge, id.cred)

  const got = readAttested(signed, challenge, id.rootHex)
  t.ok(got, 'the proof verifies')
  t.is(got.root, id.rootHex, 'and resolves to the identity, not to the device')
  t.ok(got.device, 'while naming which device signed')
})

test('⛔ a proof over DIFFERENT bytes does not verify', async (t) => {
  // Otherwise a captured proof would be a bearer token: replayable against any challenge.
  const id = await identity()
  const signed = signAttested(b4a.from('challenge-a'), id.cred)
  t.absent(readAttested(signed, b4a.from('challenge-b'), id.rootHex))
})

test('⛔ a proof for ANOTHER identity does not verify against this root', async (t) => {
  const mine = await identity()
  const theirs = await identity()
  const challenge = b4a.from('same-challenge')
  const signed = signAttested(challenge, theirs.cred)
  t.absent(readAttested(signed, challenge, mine.rootHex), 'their proof is not my identity')
  t.ok(readAttested(signed, challenge, theirs.rootHex), 'and is their own')
})

test('⛔ malformed input is REFUSED, never thrown', async (t) => {
  // The wrapper's whole reason to exist. A verifier that throws on a hostile proof turns a refusal
  // into an outage.
  const challenge = b4a.from('x')
  for (const bad of [null, undefined, '', 'not-base64!!', b4a.from([1, 2, 3]), b4a.alloc(0)]) {
    let out
    t.execution(() => { out = readAttested(bad, challenge, 'ab'.repeat(32)) }, 'survives ' + JSON.stringify(String(bad).slice(0, 12)))
    t.absent(out, 'and refuses it')
  }
})

test('⚠ no disk, no network, no clock — the property a bridge depends on', async (t) => {
  // Asserted on the source, because it is the reason admission can be proved to a service that
  // holds nothing: a verifier needing storage or a fetch could not be keyless.
  const fs = await import('#node:fs')
  const src = fs.default.readFileSync(new URL('../lib/attestation.js', import.meta.url).pathname, 'utf8')
  const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*/g, '')
  for (const forbidden of ['readFile', 'writeFile', 'fetch(', 'Date.now', 'new Date']) {
    t.absent(code.includes(forbidden), 'no ' + forbidden)
  }
})