OverviewFilesCommitsIssuesDocsReleasesPeers

trust-map / test / trust-map.test.js

242 lines · javascript ref:
import test from 'brittle'
import Corestore from 'corestore'
import IdentityKey from 'keet-identity-key'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import { TrustMap } from '../index.js'

async function makeTrustMap (t, opts = {}) {
  const store = new Corestore('/tmp/trust-map-test-' + Math.random().toString(36).slice(2))
  await store.ready()
  const core = store.get({ name: 'trust-map' })
  await core.ready()
  const tm = new TrustMap({ core, identityKey: opts.identityKey })
  await tm.ready()
  t.teardown(async () => {
    await tm.close()
    await store.close()
  })
  return tm
}

// ── Direct grants (no chain) ─────────────────────────────────

test('grant + check + revoke (direct pubkey)', async (t) => {
  const tm = await makeTrustMap(t)
  const pk = b4a.toString(crypto.randomBytes(32), 'hex')

  await tm.grant(pk, 'repo-x', ['read', 'write'])
  t.ok(await tm.check(pk, 'repo-x', 'read'), 'has read')
  t.ok(await tm.check(pk, 'repo-x', 'write'), 'has write')
  t.absent(await tm.check(pk, 'repo-x', 'admin'), 'no admin')

  await tm.revoke(pk, 'repo-x', ['write'])
  t.ok(await tm.check(pk, 'repo-x', 'read'), 'still has read')
  t.absent(await tm.check(pk, 'repo-x', 'write'), 'write revoked')

  await tm.revoke(pk, 'repo-x')
  t.absent(await tm.check(pk, 'repo-x', 'read'), 'all perms revoked')
})

test('grant accepts Buffer pubkey too', async (t) => {
  const tm = await makeTrustMap(t)
  const pkBuf = crypto.randomBytes(32)
  const pkHex = b4a.toString(pkBuf, 'hex')

  await tm.grant(pkBuf, 'repo-y', ['read'])
  t.ok(await tm.check(pkBuf, 'repo-y', 'read'), 'check by Buffer')
  t.ok(await tm.check(pkHex, 'repo-y', 'read'), 'check by hex string')
})

test('multiple grants merge perms (no duplicates)', async (t) => {
  const tm = await makeTrustMap(t)
  const pk = b4a.toString(crypto.randomBytes(32), 'hex')

  await tm.grant(pk, 'repo-x', ['read'])
  await tm.grant(pk, 'repo-x', ['write'])
  await tm.grant(pk, 'repo-x', ['read', 'admin']) // re-adds 'read'

  const entries = await tm.listForPubkey(pk)
  t.is(entries.length, 1)
  t.alike(entries[0].perms.sort(), ['admin', 'read', 'write'])
})

test('wildcard resource grants apply to all resources', async (t) => {
  const tm = await makeTrustMap(t)
  const pk = b4a.toString(crypto.randomBytes(32), 'hex')

  await tm.grant(pk, '*', ['read'])
  t.ok(await tm.check(pk, 'repo-a', 'read'), 'wildcard grants read on repo-a')
  t.ok(await tm.check(pk, 'repo-b', 'read'), 'wildcard grants read on repo-b')
  t.absent(await tm.check(pk, 'repo-a', 'write'), 'wildcard does not grant write')

  // Specific grant overlays wildcard
  await tm.grant(pk, 'repo-a', ['write'])
  t.ok(await tm.check(pk, 'repo-a', 'write'), 'specific resource grant works')
  t.ok(await tm.check(pk, 'repo-a', 'read'), 'wildcard still grants read')
})

test('listForPubkey + list', async (t) => {
  const tm = await makeTrustMap(t)
  const pk1 = b4a.toString(crypto.randomBytes(32), 'hex')
  const pk2 = b4a.toString(crypto.randomBytes(32), 'hex')

  await tm.grant(pk1, 'repo-a', ['read', 'write'])
  await tm.grant(pk1, 'repo-b', ['read'])
  await tm.grant(pk2, 'repo-a', ['admin'])

  const pk1Entries = await tm.listForPubkey(pk1)
  t.is(pk1Entries.length, 2)

  const all = await tm.list()
  t.is(all.length, 3)
})

test('persistence across reopen', async (t) => {
  const dir = '/tmp/trust-map-persist-' + Math.random().toString(36).slice(2)
  const store1 = new Corestore(dir)
  await store1.ready()
  const core1 = store1.get({ name: 'tm' })
  await core1.ready()
  const tm1 = new TrustMap({ core: core1 })
  await tm1.ready()

  const pk = b4a.toString(crypto.randomBytes(32), 'hex')
  await tm1.grant(pk, 'repo-x', ['read'])
  await tm1.close()
  await store1.close()

  // Reopen
  const store2 = new Corestore(dir)
  await store2.ready()
  const core2 = store2.get({ name: 'tm' })
  await core2.ready()
  const tm2 = new TrustMap({ core: core2 })
  await tm2.ready()

  t.ok(await tm2.check(pk, 'repo-x', 'read'), 'state persisted across reopen')

  await tm2.close()
  await store2.close()
})

// ── Chain verification (via keet-identity-key) ───────────────

test('checkChain: verifies a real hyperident-style attestation chain', async (t) => {
  const tm = await makeTrustMap(t, { identityKey: IdentityKey })

  // Create a master identity from a mnemonic
  const mnemonic = IdentityKey.generateMnemonic()
  const masterIdentity = await IdentityKey.from({ mnemonic })

  // Create a device keypair
  const deviceKeyPair = crypto.keyPair()

  // Master attests the device (this is what hyperident does internally)
  const deviceProof = await masterIdentity.bootstrap(deviceKeyPair.publicKey)

  // Sign some data with the device chain
  const data = b4a.from('hello, trust-map')
  const proof = IdentityKey.attestData(data, deviceKeyPair, deviceProof)

  // Get the master pubkey (this is what trust-map looks up)
  const masterPubkey = b4a.toString(masterIdentity.identityKeyPair.publicKey, 'hex')

  // Grant permissions to the master, NOT the device
  await tm.grant(masterPubkey, 'repo-x', ['read', 'write'])

  // The peer presents the proof + data — trust-map should accept
  const result = await tm.checkChain({
    proof,
    data,
    resource: 'repo-x',
    perm: 'write'
  })

  t.ok(result.ok, 'chain accepted')
  t.is(result.masterPubkey, masterPubkey, 'extracted correct master')
  t.is(result.devicePubkey, b4a.toString(deviceKeyPair.publicKey, 'hex'), 'extracted device pubkey')
})

test('checkChain: rejects when master is not in trust map', async (t) => {
  const tm = await makeTrustMap(t, { identityKey: IdentityKey })

  const masterIdentity = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
  const deviceKeyPair = crypto.keyPair()
  const deviceProof = await masterIdentity.bootstrap(deviceKeyPair.publicKey)

  const data = b4a.from('hello')
  const proof = IdentityKey.attestData(data, deviceKeyPair, deviceProof)

  // No grant for the master — should be rejected
  const result = await tm.checkChain({
    proof, data, resource: 'repo-x', perm: 'read'
  })

  t.absent(result.ok)
  t.ok(result.reason.includes('not authorized'), 'reason explains failure')
})

test('checkChain: rejects tampered data', async (t) => {
  const tm = await makeTrustMap(t, { identityKey: IdentityKey })

  const masterIdentity = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
  const deviceKeyPair = crypto.keyPair()
  const deviceProof = await masterIdentity.bootstrap(deviceKeyPair.publicKey)

  const data = b4a.from('original data')
  const proof = IdentityKey.attestData(data, deviceKeyPair, deviceProof)

  const masterPubkey = b4a.toString(masterIdentity.identityKeyPair.publicKey, 'hex')
  await tm.grant(masterPubkey, 'repo-x', ['write'])

  // Try to verify with TAMPERED data
  const result = await tm.checkChain({
    proof,
    data: b4a.from('tampered data'),
    resource: 'repo-x',
    perm: 'write'
  })

  t.absent(result.ok)
  t.ok(result.reason.includes('invalid attestation'), 'rejected as invalid')
})

test('checkChain: device key NOT in trust map (only master matters)', async (t) => {
  const tm = await makeTrustMap(t, { identityKey: IdentityKey })

  const masterIdentity = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
  const deviceKeyPair = crypto.keyPair()
  const deviceProof = await masterIdentity.bootstrap(deviceKeyPair.publicKey)

  const data = b4a.from('test')
  const proof = IdentityKey.attestData(data, deviceKeyPair, deviceProof)

  const masterPubkey = b4a.toString(masterIdentity.identityKeyPair.publicKey, 'hex')
  const devicePubkeyHex = b4a.toString(deviceKeyPair.publicKey, 'hex')

  // Only grant to master
  await tm.grant(masterPubkey, 'repo-x', ['admin'])

  // Direct check on the device pubkey should FAIL (device not in map)
  t.absent(await tm.check(devicePubkeyHex, 'repo-x', 'admin'), 'device not directly authorized')

  // Chain check should SUCCEED (device inherits from master)
  const result = await tm.checkChain({ proof, data, resource: 'repo-x', perm: 'admin' })
  t.ok(result.ok, 'chain check inherits from master')
})

test('checkChain: returns useful error when identityKey not configured', async (t) => {
  const tm = await makeTrustMap(t) // no identityKey

  const result = await tm.checkChain({
    proof: b4a.from('fake'),
    data: b4a.from('fake'),
    resource: 'r',
    perm: 'p'
  })

  t.absent(result.ok)
  t.ok(result.reason.includes('no identityKey'))
})