OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / keet-verifier.test.js

122 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'

import { verifyEnvelope, signTestEnvelope, EnvelopeError } from '../lib/auth/envelope.js'
import { ReplayCache } from '../lib/auth/replay-cache.js'
import { createKeetVerifier, createTestChain } from '../lib/auth/keet-verifier.js'

// Phase 2b tests — keet device-chain verification wired into envelopes.
//
// createTestChain() spins up a fresh keet identity + one attested device
// (via IdentityKey.bootstrap), returning both an anproto keypair string
// for signing envelopes AND the base64-encoded chain proof for the
// envelope.chain field.

test('keet: chain-bearing envelope resolves to identity root, not device', async (t) => {
  const { anKeypair, chainB64, rootHex, deviceHex } = await createTestChain()

  const { payload, sig } = await signTestEnvelope({
    keypair: anKeypair,
    method: 'whoami',
    params: {}
  })

  const verified = await verifyEnvelope(
    { payload, sig, chain: chainB64 },
    { replayCache: new ReplayCache(), keetVerifier: createKeetVerifier() }
  )

  t.is(verified.method, 'keet', 'method is keet when chain resolves')
  t.is(verified.rootPubkey, rootHex, 'rootPubkey is the chain identity root')
  t.is(verified.devicePubkey, deviceHex, 'devicePubkey is the signing device')
  t.not(verified.rootPubkey, verified.devicePubkey, 'root differs from device')
})

test('keet: chain for a different device is rejected', async (t) => {
  const chainA = await createTestChain()
  const chainB = await createTestChain()

  // Sign envelope with chainA's device, but attach chainB's chain. The
  // chain vouches for chainB's device — the signer is chainA's device —
  // binding check should fail.
  const { payload, sig } = await signTestEnvelope({
    keypair: chainA.anKeypair,
    method: 'x',
    params: {}
  })

  try {
    await verifyEnvelope(
      { payload, sig, chain: chainB.chainB64 },
      { replayCache: new ReplayCache(), keetVerifier: createKeetVerifier() }
    )
    t.fail('should have thrown: chain/device mismatch')
  } catch (err) {
    t.ok(err instanceof EnvelopeError, 'EnvelopeError thrown')
    t.is(err.code, 'CHAIN_INVALID', 'CHAIN_INVALID for device mismatch')
    t.ok(/does not match/.test(err.message), 'error mentions mismatch')
  }
})

test('keet: malformed chain bytes are rejected', async (t) => {
  const { anKeypair } = await createTestChain()
  const { payload, sig } = await signTestEnvelope({
    keypair: anKeypair, method: 'x', params: {}
  })

  try {
    await verifyEnvelope(
      { payload, sig, chain: 'not-a-real-chain' },
      { replayCache: new ReplayCache(), keetVerifier: createKeetVerifier() }
    )
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err instanceof EnvelopeError, 'EnvelopeError thrown')
    t.is(err.code, 'CHAIN_INVALID', 'CHAIN_INVALID for garbage chain')
  }
})

test('keet: chain-bearing envelope with empty chain string is rejected', async (t) => {
  const { anKeypair } = await createTestChain()
  const { payload, sig } = await signTestEnvelope({
    keypair: anKeypair, method: 'x', params: {}
  })

  try {
    await verifyEnvelope(
      { payload, sig, chain: '' },
      { replayCache: new ReplayCache(), keetVerifier: createKeetVerifier() }
    )
    // Empty string is falsy, so envelope.js takes the non-chain path.
    // Caller sees a normal self-rooted result. This test just pins that
    // behavior so we notice if it changes.
    t.pass('empty chain treated as absent (self-rooted path)')
  } catch (err) {
    t.fail('empty chain should not throw — it is the self-rooted path: ' + err.message)
  }
})

test('keet: two different envelopes signed by the same device both resolve to same root', async (t) => {
  const { anKeypair, chainB64, rootHex } = await createTestChain()

  const a = await signTestEnvelope({ keypair: anKeypair, method: 'one', params: {} })
  const b = await signTestEnvelope({ keypair: anKeypair, method: 'two', params: {} })

  const replayCache = new ReplayCache()
  const keetVerifier = createKeetVerifier()

  const resA = await verifyEnvelope({ ...a, chain: chainB64 }, { replayCache, keetVerifier })
  const resB = await verifyEnvelope({ ...b, chain: chainB64 }, { replayCache, keetVerifier })

  t.is(resA.rootPubkey, rootHex, 'A → identity root')
  t.is(resB.rootPubkey, rootHex, 'B → identity root')
  t.is(resA.rootPubkey, resB.rootPubkey, 'both envelopes share the same root')
  t.is(resA.devicePubkey, resB.devicePubkey, 'both envelopes from same device')
})

test('keet: chain for attested envelope decodes to a non-empty buffer', async (t) => {
  const { chainB64 } = await createTestChain()
  const buf = b4a.from(chainB64, 'base64')
  t.ok(buf.length > 32, 'chain buffer is non-trivial (> 32 bytes)')
})