// identity-attest.test.js — identity.attest / identity.verify (hyper-games #89 §3.3).
//
// A game peer proves "this mesh key is keet member X": identity.attest signs
// arbitrary data with this daemon's keet device credentials (IdentityKey.attestData
// — keet-core's internalAttestData); identity.verify checks such a proof for a
// given memberId (IdentityKey.verify, pure). The receiver verifies the claim
// against the DHT key the Noise handshake authenticated, then shows that member's
// ROSTER name.
//
// On fresh tmp storage, two identities (A = identity-0, B = identity-1):
// 1. attest under A, then under B (switch) — distinct memberIds
// 2. verify accepts A's proof for (data, A) and B's for (data, B)
// 3. verify rejects: other data, the other memberId, a tampered proof, garbage
//
// Run: cd hyper-fleeky/packages/keet-daemon && bare test/identity-attest.test.js
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Pipe from 'bare-pipe'
import FramedStream from 'framed-stream'
import { startDaemon } from '../index.js'
let exitCode = 0
const failures = []
function ok (cond, msg) {
if (cond) console.log(' ok ' + msg)
else { console.log(' FAIL ' + msg); exitCode = 1; failures.push(msg) }
}
function rmrf (dir) { try { fs.rmSync(dir, { recursive: true, force: true }) } catch {} }
function mktemp (prefix) {
const dir = path.join(os.tmpdir(), 'keet-daemon-test-' + prefix + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8))
fs.mkdirSync(dir, { recursive: true })
return dir
}
class Client {
constructor (socketPath) {
this._socketPath = socketPath
this._pipe = null
this._framed = null
this._nextId = 1
this._pending = new Map()
}
connect () {
return new Promise((resolve, reject) => {
const pipe = new Pipe(this._socketPath)
const onErr = (err) => reject(err)
pipe.once('error', onErr)
pipe.once('connect', () => {
pipe.removeListener('error', onErr)
this._pipe = pipe
const FS = FramedStream.default || FramedStream
this._framed = new FS(pipe)
this._framed.on('data', (buf) => {
let msg
try { msg = JSON.parse(buf.toString('utf-8')) } catch { return }
const p = this._pending.get(msg.id)
if (!p) return
this._pending.delete(msg.id)
if (msg.ok === false || msg.error) p.reject(new Error(msg.error || 'RPC failed'))
else p.resolve(msg.data !== undefined ? msg.data : msg)
})
resolve()
})
})
}
call (method, params = {}, timeoutMs = 30000) {
const id = this._nextId++
const payload = JSON.stringify({ id, method, params })
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this._pending.delete(id)
reject(new Error('RPC timeout: ' + method))
}, timeoutMs)
this._pending.set(id, {
resolve: (v) => { clearTimeout(timer); resolve(v) },
reject: (e) => { clearTimeout(timer); reject(e) }
})
this._framed.write(Buffer.from(payload, 'utf-8'))
})
}
close () {
try { this._framed && this._framed.destroy() } catch {}
try { this._pipe && this._pipe.destroy() } catch {}
}
}
async function verify (client, params) {
try {
const r = await client.call('identity.verify', params, 15000)
return r && r.ok === true
} catch (err) {
return 'threw: ' + err.message
}
}
async function main () {
const baseDir = mktemp('identity-attest')
const socketPath = path.join(baseDir, 'daemon.sock')
console.log('[test] baseDir = ' + baseDir)
let daemon
try {
daemon = await startDaemon({ storage: baseDir, socketPath })
ok(true, 'startDaemon resolved')
} catch (err) {
ok(false, 'startDaemon: ' + err.message); rmrf(baseDir); Bare.exit(1); return
}
const client = new Client(socketPath)
try { await client.connect(); ok(true, 'UDS connected') }
catch (err) { ok(false, 'UDS connect: ' + err.message); rmrf(baseDir); Bare.exit(1); return }
// A 32-byte mesh key, as the game would send it (hex).
const data = Buffer.alloc(32, 7).toString('hex')
const other = Buffer.alloc(32, 9).toString('hex')
let a = null
let b = null
try {
a = await client.call('identity.attest', { data }, 15000)
const idA = await client.call('identity', {}, 15000)
ok(a && typeof a.proof === 'string' && a.proof.length > 0, 'attest under A returns a proof')
ok(a && a.memberId === idA.memberId, 'attest reports A\'s memberId (' + (a && a.memberId) + ')')
const created = await client.call('identity-manager.create', { name: 'attest-b-' + Date.now().toString(36) }, 15000)
const newId = created.id || (created.entry && created.entry.id)
await client.call('identity-manager.switch', { id: newId }, 30000)
b = await client.call('identity.attest', { data }, 15000)
ok(b && b.memberId && b.memberId !== a.memberId, 'attest under B reports a different memberId (' + (b && b.memberId) + ')')
} catch (err) { ok(false, 'attest: ' + err.message) }
if (a && b) {
ok(await verify(client, { proof: a.proof, data, memberId: a.memberId }) === true, 'A\'s proof verifies for (data, A)')
ok(await verify(client, { proof: b.proof, data, memberId: b.memberId }) === true, 'B\'s proof verifies for (data, B)')
ok(await verify(client, { proof: a.proof, data: other, memberId: a.memberId }) === false, 'A\'s proof does NOT verify for other data')
ok(await verify(client, { proof: a.proof, data, memberId: b.memberId }) === false, 'A\'s proof does NOT verify as B (impersonation)')
ok(await verify(client, { proof: b.proof, data, memberId: a.memberId }) === false, 'B\'s proof does NOT verify as A (impersonation)')
const tampered = a.proof.slice(0, -2) + (a.proof.slice(-2) === '00' ? '01' : '00')
ok(await verify(client, { proof: tampered, data, memberId: a.memberId }) === false, 'a tampered proof does NOT verify')
ok(await verify(client, { proof: 'zz', data, memberId: a.memberId }) === false, 'garbage proof → ok:false, not a throw')
ok(await verify(client, { proof: a.proof, data, memberId: 'not-a-member-id' }) === false, 'garbage memberId → ok:false, not a throw')
}
console.log('[test] cleanup')
client.close()
try { await daemon.router.core.close() } catch {}
try { await daemon.core.close() } catch {}
try { await daemon.uds.stop() } catch {}
rmrf(baseDir)
console.log('')
if (exitCode === 0) console.log('=== RESULT: ALL PASSED ===')
else {
console.log('=== RESULT: ' + failures.length + ' FAILED ===')
for (const f of failures) console.log(' - ' + f)
}
Bare.exit(exitCode)
}
main().catch(err => {
console.error('[test] uncaught:', err && err.message)
if (err && err.stack) console.error(err.stack)
Bare.exit(1)
})