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.

72 lines (67 loc) • 5.33 kB
#!/usr/bin/env node // Second, independent-code check of the vectors using @mdip/cipher (what @mdip/keymaster and the // gatekeeper use): recomputes JCS bytes from the JSON files, compares to the committed .bin files, // and verifies each signature. Also checks the MDIP binding fields of the challenge fixture. // Usage: node vectors/verify-with-kit.js (after npm install; no network) import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import CipherNode from '@mdip/cipher/node'; import { checkResponseExpiry } from '../src/index.js'; const dir = path.dirname(fileURLToPath(import.meta.url)); const c = new CipherNode(); const rd = f => fs.readFileSync(path.join(dir, f), 'utf8'); const js = f => JSON.parse(rd(f)); let fail = 0; const check = (cond, msg) => { console.log(`${cond ? 'ok ' : 'FAIL'} ${msg}`); if (!cond) fail = 1; }; const unsigned = o => { const x = JSON.parse(JSON.stringify(o)); delete x.signature; return x; }; // kav-1 const signed = js('kav-1/signed.json'); const key = js('kav-1/test-key.json').publicJwk; check(c.canonicalizeJSON(unsigned(signed)) === rd('kav-1/message.bin'), 'kav-1 message.bin == JCS(signed.json minus signature)'); check(c.hashJSON(unsigned(signed)) === signed.signature.hash, 'kav-1 signature.hash == sha256(JCS)'); check(c.verifySig(signed.signature.hash, signed.signature.value, key), 'kav-1 verifySig'); check(rd('kav-1/signature.hex').trim() === signed.signature.value, 'kav-1 signature.hex == signature.value'); // mdip-challenge-1 const dids = js('mdip-challenge-1/dids.json'); const op = k => js(`mdip-challenge-1/${k}.op.json`); const keys = { [dids['verifier-agent']]: op('verifier-agent').publicJwk, [dids['responder-agent']]: op('responder-agent').publicJwk }; for (const k of ['verifier-agent', 'challenge', 'responder-agent', 'response']) { const o = op(k); check(c.canonicalizeJSON(unsigned(o)) === rd(`mdip-challenge-1/${k}.message.bin`), `${k} message.bin == JCS(op minus signature)`); check(c.hashJSON(unsigned(o)) === o.signature.hash, `${k} signature.hash`); // agent create ops are self-signed with their own publicJwk (no signer DID yet); assets by their controller const signer = o.mdip.type === 'agent' ? (k === 'verifier-agent' ? dids['verifier-agent'] : dids['responder-agent']) : o.controller; check(c.verifySig(o.signature.hash, o.signature.value, o.mdip.type === 'agent' ? o.publicJwk : keys[signer]), `${k} signed by ${signer === dids['verifier-agent'] ? 'verifier' : 'responder'} key`); } const ch = op('challenge'), re = op('response'), vr = js('mdip-challenge-1/verify-response.json'); check(ch.controller === dids['verifier-agent'], 'challenge controller == verifier DID (binds verifier)'); check(re.controller === dids['responder-agent'] && re.data.encrypted.sender === re.controller, 'response controller == encrypted.sender == responder DID'); check(Date.parse(re.mdip.validUntil) <= Date.parse(ch.mdip.validUntil), 'response validUntil <= challenge validUntil (expiry)'); check(vr.challenge === dids.challenge && vr.responder === dids['responder-agent'] && vr.match === true && !!vr.responseNonce, 'decrypted response names the challenge DID, responder, match=true, has responseNonce'); // mdip-challenge-2: PUBLIC TEST verifier key -> decrypt the response and check the binding offline { const d = 'mdip-challenge-2'; const dids2 = js(`${d}/dids.json`), o2 = k => js(`${d}/${k}.op.json`), tv = js(`${d}/test-verifier.json`); const ch2 = o2('challenge'), re2 = o2('response'), resp = o2('responder-agent'), ver = o2('verifier-agent'); check(JSON.stringify(tv.publicJwk) === JSON.stringify(ver.publicJwk) && tv.did === dids2['verifier-agent'], `${d} test-verifier.json key == verifier-agent op key`); for (const k of ['verifier-agent', 'challenge', 'responder-agent', 'response']) { const o = o2(k); check(c.canonicalizeJSON(unsigned(o)) === rd(`${d}/${k}.message.bin`) && c.canonicalizeJSON(o) === rd(`${d}/${k}.signed-op.bin`), `${d} ${k} JCS bytes`); check(c.verifySig(o.signature.hash, o.signature.value, o.mdip.type === 'agent' ? o.publicJwk : (o.controller === dids2['verifier-agent'] ? ver : resp).publicJwk), `${d} ${k} signature`); } const pt = c.decryptMessage(resp.publicJwk, tv.privateJwk, re2.data.encrypted.cipher_receiver); check(pt === rd(`${d}/response.plaintext.json`), `${d} @mdip/cipher decrypt == response.plaintext.json`); check(JSON.parse(pt).response.challenge === dids2.challenge, `${d} BINDING: response.challenge == challenge DID`); check(ch2.controller === dids2['verifier-agent'] && re2.controller === dids2['responder-agent'], `${d} controllers`); check(checkResponseExpiry(ch2.mdip.validUntil, re2.mdip.validUntil).ok, `${d} strict expiry (response <= challenge)`); } // strict-expiry cases (same table verify-openssl.sh uses) for (const line of rd('expiry-cases.txt').split('\n')) { if (!line.trim() || line.startsWith('#')) continue; const [cv, rv, want, ...note] = line.trim().split(/\s+/); const got = checkResponseExpiry(cv === '-' ? undefined : cv, rv === '-' ? undefined : rv).ok ? 'accept' : 'reject'; check(got === want, `expiry case: ${note.join(' ')} -> ${got}`); } console.log(fail ? 'VECTORS FAILED' : 'ALL VECTORS OK'); process.exit(fail);