UNPKG

mdip-agent-kit

Version:

Give any AI agent its own MDIP (did:mdip) identity on the SELF (https://selfid.com) / Keychain hyperswarm network: keymaster wallet, DID, SELF Public Profile molecule, backups, challenges and delegated-authority credentials.

89 lines (85 loc) • 6.11 kB
#!/usr/bin/env node // Captures vectors/mdip-challenge-2/: a REAL challenge -> response where the VERIFIER is a PUBLIC TEST // IDENTITY (its wallet mnemonic and agent private key are published on purpose), so anyone can decrypt // the response offline and check the challenge <-> response binding without running this kit. // Suggested by @homies on connectthebots.app (/book/1227): in mdip-challenge-1 the binding lived only // inside ciphertext encrypted to a private verifier key. // Usage: MDIP_GATEKEEPER_URL=http://localhost:4224 node vectors/make-mdip-fixture-2.js // NOTE: publishes throwaway DIDs (two agents, one 1-hour challenge, one response) to the registry. import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; import { fileURLToPath } from 'node:url'; import * as bip39 from 'bip39'; import CipherNode from '@mdip/cipher/node'; import { openAgent, waitForDid } from '../src/index.js'; const out = path.join(path.dirname(fileURLToPath(import.meta.url)), 'mdip-challenge-2'); fs.rmSync(out, { recursive: true, force: true }); fs.mkdirSync(out, { recursive: true }); const cipher = new CipherNode(); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'kav2-fixture-')); const w = (f, data) => fs.writeFileSync(path.join(out, f), data); const der = hex => { const int = b => { let i = 0; while (i < b.length - 1 && b[i] === 0) i++; b = b.subarray(i); if (b[0] & 0x80) b = Buffer.concat([Buffer.from([0]), b]); return Buffer.concat([Buffer.from([0x02, b.length]), b]); }; const body = Buffer.concat([int(Buffer.from(hex.slice(0, 64), 'hex')), int(Buffer.from(hex.slice(64), 'hex'))]); return Buffer.concat([Buffer.from([0x30, body.length]), body]); }; // PUBLIC TEST VERIFIER. Mnemonic derived from a published label; anyone can re-derive it. Never use it. const LABEL = 'mdip-agent-kit mdip-challenge-2 PUBLIC TEST VERIFIER - never use for anything real'; const mnemonic = bip39.entropyToMnemonic(crypto.createHash('sha256').update(LABEL, 'utf8').digest().subarray(0, 16)); try { const verifier = await openAgent({ home: path.join(tmp, 'v'), name: 'kav2-public-test-verifier', create: true, mnemonic }); const responder = await openAgent({ home: path.join(tmp, 'r'), name: 'kav2-responder', create: true }); const vkp = await verifier.keymaster.fetchKeyPair(); const challengeDid = await verifier.createChallenge({}); await waitForDid(verifier.keymaster, challengeDid); const responseDid = await responder.respondToChallenge(challengeDid); // validUntil capped at challenge's await waitForDid(verifier.keymaster, responseDid); const result = await verifier.verifyResponse(responseDid); if (!result.match) throw new Error('verifyResponse did not match'); const names = { 'verifier-agent': verifier.did, 'responder-agent': responder.did, challenge: challengeDid, response: responseDid }; const exported = await verifier.keymaster.gatekeeper.exportDIDs(Object.values(names)); const ops = {}; Object.keys(names).forEach((k, i) => { ops[k] = exported[i][0].operation; }); if (JSON.stringify(ops['verifier-agent'].publicJwk) !== JSON.stringify(vkp.publicJwk)) throw new Error('verifier key mismatch'); for (const k of ['verifier-agent', 'responder-agent']) w(`${k}.pub.pem`, crypto.createPublicKey({ key: { ...ops[k].publicJwk }, format: 'jwk' }).export({ type: 'spki', format: 'pem' })); const priv = crypto.createPrivateKey({ key: { ...vkp.privateJwk }, format: 'jwk' }); w('verifier-agent.PUBLIC-TEST-KEY.priv.pem', priv.export({ type: 'pkcs8', format: 'pem' })); w('test-verifier.json', JSON.stringify({ WARNING: 'PUBLIC TEST VERIFIER - wallet mnemonic and private key are published on purpose; never use them', label: LABEL, mnemonicEntropy: 'first 16 bytes of sha256(label)', mnemonic, derivationPath: "m/44'/0'/0'/0/0", did: verifier.did, publicJwk: vkp.publicJwk, privateJwk: vkp.privateJwk, }, null, 2) + '\n'); const sums = []; for (const [k, op] of Object.entries(ops)) { const copy = JSON.parse(JSON.stringify(op)); delete copy.signature; const msg = cipher.canonicalizeJSON(copy); const hash = cipher.hashMessage(msg); if (hash !== op.signature.hash) throw new Error(`${k}: hash mismatch`); const signerKey = op.mdip.type === 'agent' ? op.publicJwk : ops[op.controller === verifier.did ? 'verifier-agent' : 'responder-agent'].publicJwk; if (!cipher.verifySig(hash, op.signature.value, signerKey)) throw new Error(`${k}: sig fails`); const full = cipher.canonicalizeJSON(op); // DID suffix = CIDv1(dag-json, sha256(JCS(signed op))) w(`${k}.op.json`, JSON.stringify(op, null, 2) + '\n'); w(`${k}.message.bin`, msg); w(`${k}.sig.der`, der(op.signature.value)); w(`${k}.signed-op.bin`, full); sums.push(`${k}.message.bin`, `${k}.sig.der`, `${k}.signed-op.bin`); } // The response ciphertext the verifier received, and the plaintext it decrypts to (exact bytes). const ct = ops.response.data.encrypted.cipher_receiver; w('response.cipher_receiver.bin', Buffer.from(ct, 'base64url')); const plaintext = cipher.decryptMessage(ops['responder-agent'].publicJwk, vkp.privateJwk, ct); w('response.plaintext.json', plaintext); if (JSON.parse(plaintext).response.challenge !== challengeDid) throw new Error('binding mismatch'); sums.push('response.cipher_receiver.bin', 'response.plaintext.json'); w('dids.json', JSON.stringify(names, null, 2) + '\n'); w('verify-response.json', JSON.stringify(result, null, 2) + '\n'); sums.push('verifier-agent.pub.pem', 'responder-agent.pub.pem', 'verifier-agent.PUBLIC-TEST-KEY.priv.pem', 'dids.json'); w('SHA256SUMS', sums.map(f => crypto.createHash('sha256').update(fs.readFileSync(path.join(out, f))).digest('hex') + ' ' + f).join('\n') + '\n'); console.log(JSON.stringify({ out, names, match: result.match, plaintext }, null, 2)); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } process.exit(0);