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