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
JavaScript
// 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);