OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / test / probe-state-attestation.js

119 lines · javascript ref:
// Probe (room-state spec P3): membership-gated admission via REAL keet
// attestation. Validates the injected attest/verifyAttest path of StateBase
// against the actual keet-identity-key library (IdentityKey.from / bootstrap /
// attestData / verify) — the same primitive keet-core uses to attest member
// profiles (keet-core/index.js:1849 internalAttestData → IdentityKey.attestData).
//
//   C = MOD     → creator, writer/indexer
//   P = PEER    → keet member with a valid identity attestation → ADMITTED
//   X = OUTSIDER → has a valid attestation for ITS OWN identity, but that
//                  identity is not in the member list → REFUSED (and it cannot
//                  forge P's identity: it lacks P's identity secret)
//
// Run: bare test/probe-state-attestation.js
// PASS = P admitted (write reaches C); X refused (non-writable).

import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import createTestnet from 'hyperdht/testnet.js'
import IdentityKey from 'keet-identity-key'
import { StateBase } from '../lib/state-base.js'

const ROOM_ID = 'probe-attest-' + Math.random().toString(36).slice(2, 10)
const roomTag = crypto.hash(Buffer.from(ROOM_ID, 'utf-8'))
const DEADLINE_MS = 30000

// Build a real keet identity + attested device (mirrors keet-core's identity).
async function makeIdentity () {
  const idKey = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
  const identityPub = idKey.identityPublicKey              // Buffer(32) — the keet identity
  const deviceKp = crypto.keyPair()                        // this device's keypair
  const deviceProof = await idKey.bootstrap(deviceKp.publicKey)  // proof: identity attests device (async!)
  return {
    identityPub,
    // attest(data) signs data with the device key + embeds the identity→device chain
    attest: (data) => IdentityKey.attestData(data, deviceKp, deviceProof)
  }
}
// Shared verifier — exactly how keet verifies a member attestation.
function verifyAttest (proof, data, expectedIdentity) {
  try { return IdentityKey.verify(proof, data, { expectedIdentity }) !== null } catch { return false }
}

const C = await makeIdentity()  // mod / creator
const P = await makeIdentity()  // peer member
const X = await makeIdentity()  // outsider (valid identity, NOT a member)

const MEMBERS = [
  { key: C.identityPub.toString('hex'), role: 2 },  // mod
  { key: P.identityPub.toString('hex'), role: 3 }   // peer
]
const getMembers = async () => MEMBERS

const dirs = {}
for (const n of ['C', 'P', 'X']) {
  dirs[n] = path.join(os.tmpdir(), 'probe-attest-' + n + '-' + Date.now())
  fs.mkdirSync(dirs[n], { recursive: true })
}

console.log('roomId:', ROOM_ID)
console.log('C(mod):', C.identityPub.toString('hex').slice(0, 12) + '…  P(peer):', P.identityPub.toString('hex').slice(0, 12) + '…  X(outsider):', X.identityPub.toString('hex').slice(0, 12) + '…')

console.log('\nspinning up local DHT testnet...')
const testnet = await createTestnet(3)
const swarmOpts = { bootstrap: testnet.bootstrap }

const mk = (dir, id) => new StateBase(dir, roomTag, {
  swarmOpts, selfKey: id.identityPub, attest: id.attest, verifyAttest, getMembers
})
const sbC = mk(dirs.C, C)
const sbP = mk(dirs.P, P)
const sbX = mk(dirs.X, X)

let exitCode = 1
function fail (m) { console.error('\n✗ FAIL:', m) }

try {
  console.log('\n[C] ready (creator mints)…'); await sbC.ready()
  console.log('[P] ready (member, awaits attestation-admission)…'); await sbP.ready()
  console.log('[X] ready (outsider, should be refused)…'); await sbX.ready()
  if (!sbC.stateBaseKey) throw new Error('creator did not mint')

  console.log('\npolling for attestation-gated admission outcomes…')
  const start = Date.now()
  let lastLog = 0
  let pWroteOk = false
  while (Date.now() - start < DEADLINE_MS) {
    const pW = !!(sbP.base && sbP.base.writable)
    const xW = !!(sbX.base && sbX.base.writable)
    if (pW && !pWroteOk) { try { await sbP.set('p:flag', 'admitted'); pWroteOk = true } catch {} }
    let cSeesP = null
    if (pWroteOk) cSeesP = await sbC.get('p:flag').catch(() => null)
    const xAdopted = !!(sbX.base && sbX.stateBaseKey)

    if (pW && pWroteOk && cSeesP && cSeesP.value === 'admitted' && xAdopted && !xW) {
      console.log('\n✓ P admitted via real keet attestation (write reached C); X (valid identity, non-member) REFUSED')
      exitCode = 0
      break
    }
    const now = Date.now()
    if (now - lastLog > 3000) { lastLog = now; console.log('  P.writable=', pW, ' C.sees=', cSeesP ? cSeesP.value : null, ' X.adopted=', xAdopted, ' X.writable=', xW) }
    await new Promise(r => setTimeout(r, 500))
  }
  if (exitCode !== 0) fail('expected P attestation-admitted and X refused; P.writable=' + !!(sbP.base && sbP.base.writable) + ' X.writable=' + !!(sbX.base && sbX.base.writable))
  if (sbX.base && sbX.base.writable) { fail('SECURITY: outsider X became writable'); exitCode = 1 }
} catch (err) {
  fail(err.message); if (err.stack) console.error(err.stack)
} finally {
  await Promise.race([
    (async () => { for (const sb of [sbC, sbP, sbX]) await sb.close().catch(() => {}); await testnet.destroy().catch(() => {}) })(),
    new Promise(r => setTimeout(r, 3000))
  ])
  for (const n of ['C', 'P', 'X']) { try { fs.rmSync(dirs[n], { recursive: true, force: true }) } catch {} }
}

console.log(exitCode === 0 ? '\nPASS' : '\nFAIL')
Bare.exit(exitCode)