| … | … | @@ -0,0 +1,70 @@ |
| 1 | // PHASE 0 #3 — keet-ATTESTATION ADMISSION. Real co-owner devices don't sign admission with the member |
| 2 | // key directly; a DEVICE (attested to the member identity) signs, and the verifier checks the |
| 3 | // attestation. atproto-base threads injectable attest/verifyAttest (the model hpp uses via |
| 4 | // atproto-room/lib/membership.js). Here a mock attestation scheme exercises the wiring: a device |
| 5 | // attested to its member is admitted; a device NOT attested is rejected. |
| 6 | |
| 7 | import test from 'brittle' |
| 8 | import { mkdtempSync, rmSync } from 'node:fs' |
| 9 | import { tmpdir } from 'node:os' |
| 10 | import { join } from 'node:path' |
| 11 | import crypto from 'hypercore-crypto' |
| 12 | import { deriveAtprotoKey, createAtprotoIdentity } from 'atproto-identity' |
| 13 | import { AtprotoBase } from '../lib/base.js' |
| 14 | |
| 15 | const MNEMONIC = 'test test test test test test test test test test test junk' |
| 16 | const ID = createAtprotoIdentity(MNEMONIC, { handle: 'x.7rnx.net', pdsUrl: 'https://pds.7rnx.net' }) |
| 17 | const KEY = deriveAtprotoKey(MNEMONIC) |
| 18 | const post = (text) => ({ $type: 'app.bsky.feed.post', text, createdAt: '2026-07-23T00:00:00.000Z', langs: ['en'] }) |
| 19 | const hex = (b) => b.toString('hex') |
| 20 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) |
| 21 | function mk (opts) { const dir = mkdtempSync(join(tmpdir(), 'att-')); const base = new AtprotoBase({ path: dir, did: ID.did, privateKey: KEY.privateKey, ackInterval: 100, ...opts }); return { base, dir, close: async () => { try { await base.close() } catch {} ; rmSync(dir, { recursive: true, force: true }) } } } |
| 22 | const pipe = (a, b) => { const s1 = a.base.base.replicate(true); const s2 = b.base.base.replicate(false); s1.pipe(s2).pipe(s1); return () => { try { s1.destroy() } catch {} ; try { s2.destroy() } catch {} } } |
| 23 | async function settle (...ws) { for (let i = 0; i < 20; i++) { for (const w of ws) { try { await w.base.base.update() } catch {} } await sleep(60) } } |
| 24 | |
| 25 | // Mock keet-attestation: a member key ATTESTS a device (cert = member signs deviceKey). attest signs |
| 26 | // signData with the device; verifyAttest checks (a) the member attested the device, (b) the device |
| 27 | // signed signData. Contract: attest(signData)->Buffer; verifyAttest(proof, signData, identity)->bool. |
| 28 | const makeAttest = (deviceKp, memberKp) => { |
| 29 | const cert = crypto.sign(deviceKp.publicKey, memberKp.secretKey) |
| 30 | return async (signData) => Buffer.from(JSON.stringify({ device: hex(deviceKp.publicKey), cert: hex(cert), sig: hex(crypto.sign(signData, deviceKp.secretKey)) })) |
| 31 | } |
| 32 | const makeUnattested = (deviceKp) => { // cert signed by a RANDOM key → the member never attested this device |
| 33 | const cert = crypto.sign(deviceKp.publicKey, crypto.keyPair().secretKey) |
| 34 | return async (signData) => Buffer.from(JSON.stringify({ device: hex(deviceKp.publicKey), cert: hex(cert), sig: hex(crypto.sign(signData, deviceKp.secretKey)) })) |
| 35 | } |
| 36 | const verifyAttest = async (proof, signData, identity) => { |
| 37 | try { |
| 38 | const p = JSON.parse(proof.toString()); const device = Buffer.from(p.device, 'hex') |
| 39 | if (!crypto.verify(device, Buffer.from(p.cert, 'hex'), identity)) return false // did `identity` attest this device? |
| 40 | return crypto.verify(signData, Buffer.from(p.sig, 'hex'), device) // did the device sign signData? |
| 41 | } catch { return false } |
| 42 | } |
| 43 | |
| 44 | test('keet-attestation: an attested device is admitted and its write converges', async (t) => { |
| 45 | const cM = crypto.keyPair(); const cD = crypto.keyPair(); const jM = crypto.keyPair(); const jD = crypto.keyPair() |
| 46 | const roster = [{ key: hex(cM.publicKey), role: 1 }, { key: hex(jM.publicKey), role: 1 }] |
| 47 | |
| 48 | const C = mk({ identity: { publicKey: cM.publicKey }, members: roster, attest: makeAttest(cD, cM), verifyAttest }); await C.base.ready() |
| 49 | const J = mk({ bootstrap: C.base.base.key, identity: { publicKey: jM.publicKey }, attest: makeAttest(jD, jM), verifyAttest }); await J.base.ready() |
| 50 | const un = pipe(C, J); t.teardown(async () => { un(); await C.close(); await J.close() }) |
| 51 | |
| 52 | await C.base.projectMembers(roster); await settle(C, J) |
| 53 | await J.base.putRecord('app.bsky.feed.post', '3j', post('attested joiner')) |
| 54 | t.ok(J.base.writable, 'attested device was admitted (keet-attestation proof accepted)') |
| 55 | await settle(C, J); await C.base.settled() |
| 56 | t.ok((await C.base.indexedRecords()).find((r) => r.key.endsWith('3j')), "joiner's record converged into the shared view") |
| 57 | }) |
| 58 | |
| 59 | test('keet-attestation: a device NOT attested to the member is rejected', async (t) => { |
| 60 | const cM = crypto.keyPair(); const cD = crypto.keyPair(); const jM = crypto.keyPair(); const jD = crypto.keyPair() |
| 61 | const roster = [{ key: hex(cM.publicKey), role: 1 }, { key: hex(jM.publicKey), role: 1 }] |
| 62 | |
| 63 | const C = mk({ identity: { publicKey: cM.publicKey }, members: roster, attest: makeAttest(cD, cM), verifyAttest }); await C.base.ready() |
| 64 | const J = mk({ bootstrap: C.base.base.key, identity: { publicKey: jM.publicKey }, attest: makeUnattested(jD), verifyAttest }); await J.base.ready() |
| 65 | const un = pipe(C, J); t.teardown(async () => { un(); await C.close(); await J.close() }) |
| 66 | |
| 67 | await C.base.projectMembers(roster); await settle(C, J) |
| 68 | await t.exception(() => J.base.ensureWritable({ timeoutMs: 2500 }), 'unattested device is rejected (throws — not admitted)') |
| 69 | t.absent(J.base.writable, 'never became a writer') |
| 70 | }) |