diff --git a/examples/js/ras-crew-poc/build-registry.js b/examples/js/ras-crew-poc/build-registry.js new file mode 100644 index 0000000..b419d0a --- /dev/null +++ b/examples/js/ras-crew-poc/build-registry.js @@ -0,0 +1,220 @@ +#!/usr/bin/env node +/** + * RAS Crew did:trail PoC - Registry Builder + * + * Generates one ed25519 keypair, computes spec-conformant did:trail identifiers + * for 1 org (Rocking.AI.Sales) + 5 agents (AI Sales Crew), writes a signed + * registry JSON and exports the public key. + * + * Spec refs: + * - §4.1 ABNF: only self/org/agent modes allowed + * - §4.5.1 slug normalization + agent slug = -- + * - §4.5.2 trail-hash = SHA-256(slug + ":" + publicKeyMultibase)[0:16] + * - §3.3.1 TrailRegistryService endpoint REQUIRED + * + * Run: + * node build-registry.js + * + * Outputs: + * ../../ras-crew-registry.json (registry with 6 DID Documents + proof) + * ../../keys/ras-crew-poc.pub (public key: multibase + JWK) + * ~/.config/ras-crew-poc-signing.json (PRIVATE key - outside repo, never commit) + */ + +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// ---------- base58btc (for multibase z-prefix) ---------- +const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; +function base58btcEncode(bytes) { + if (bytes.length === 0) return ''; + const digits = [0]; + for (let i = 0; i < bytes.length; i++) { + let carry = bytes[i]; + for (let j = 0; j < digits.length; j++) { + carry += digits[j] << 8; + digits[j] = carry % 58; + carry = (carry / 58) | 0; + } + while (carry > 0) { digits.push(carry % 58); carry = (carry / 58) | 0; } + } + let zeros = 0; + for (let k = 0; k < bytes.length && bytes[k] === 0; k++) zeros++; + return '1'.repeat(zeros) + digits.reverse().map(d => ALPHABET[d]).join(''); +} + +// Multibase Ed25519 public key: 0xed01 prefix + 32-byte raw key, base58btc, 'z' prefix +function ed25519ToMultibase(raw32) { + const prefixed = Buffer.concat([Buffer.from([0xed, 0x01]), raw32]); + return 'z' + base58btcEncode(prefixed); +} + +function base64url(buf) { + return Buffer.from(buf).toString('base64').replace(/=+$/,'').replace(/\+/g,'-').replace(/\//g,'_'); +} + +// ---------- trail-hash per §4.5.2 ---------- +function trailHash(slug, publicKeyMultibase) { + return crypto.createHash('sha256').update(slug + ':' + publicKeyMultibase).digest('hex').slice(0, 16); +} + +// ---------- canonical JSON (RFC 8785 subset: sorted keys, no whitespace) ---------- +function canonicalize(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) return '[' + value.map(canonicalize).join(',') + ']'; + const keys = Object.keys(value).sort(); + return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}'; +} + +// ---------- main ---------- +const ORG_SLUG = 'rocking-ai-sales'; +const AGENTS = [ + { slug: 'rocking-ai-sales-recherche-01', name: 'Recherche-Agent', role: 'Research / ICP scoring' }, + { slug: 'rocking-ai-sales-erstkontakt-01', name: 'Erstkontakt-Agent', role: 'Outreach drafting' }, + { slug: 'rocking-ai-sales-qualifizierung-01', name: 'Qualifizierungs-Agent', role: 'PRISM qualification' }, + { slug: 'rocking-ai-sales-vorbereitung-01', name: 'Vorbereitungs-Agent', role: 'Meeting prep' }, + { slug: 'rocking-ai-sales-nachfass-01', name: 'Nachfass-Agent', role: 'Follow-up / action items' }, +]; + +// Generate ONE ed25519 keypair (PoC: all 6 DIDs share the same key) +const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519'); +const rawPub = publicKey.export({ format: 'der', type: 'spki' }).slice(-32); +const rawPriv = privateKey.export({ format: 'der', type: 'pkcs8' }).slice(-32); +const pubMultibase = ed25519ToMultibase(rawPub); +const pubJwk = { kty: 'OKP', crv: 'Ed25519', x: base64url(rawPub) }; + +// Compute DIDs +const orgHash = trailHash(ORG_SLUG, pubMultibase); +const orgDid = `did:trail:org:${ORG_SLUG}-${orgHash}`; + +const agentDocs = AGENTS.map(a => { + const h = trailHash(a.slug, pubMultibase); + const did = `did:trail:agent:${a.slug}-${h}`; + return { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://trailprotocol.org/ns/did/v1', + ], + id: did, + controller: orgDid, + 'trail:trailMode': 'agent', + 'trail:aiSystemType': 'agent', + 'trail:euAiActRiskClass': 'minimal', + 'trail:parentOrganization': orgDid, + 'trail:displayName': a.name, + 'trail:description': `Rocking.AI.Sales Crew - ${a.role}`, + 'trail:humanOversight': { + name: 'Christian Hommrich', + email: 'christian.hommrich@rockingaisales.de', + role: 'Founder / Operator', + }, + verificationMethod: [{ + id: `${did}#key-1`, + type: 'JsonWebKey2020', + controller: did, + publicKeyJwk: pubJwk, + }], + authentication: [`${did}#key-1`], + assertionMethod: [`${did}#key-1`], + service: [{ + id: `${did}#trail-registry`, + type: 'TrailRegistryService', + serviceEndpoint: `https://trailprotocol.org/verify/ras-crew/${a.slug.replace(/^rocking-ai-sales-/, '')}/`, + }], + _meta: { slug: a.slug, displayName: a.name }, + }; +}); + +const orgDoc = { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://trailprotocol.org/ns/did/v1', + ], + id: orgDid, + controller: orgDid, + 'trail:trailMode': 'org', + 'trail:displayName': 'Rocking.AI.Sales', + 'trail:legalName': 'Rocking.AI.Sales (pre-incorporation, Christian Hommrich)', + 'trail:jurisdiction': 'DE', + verificationMethod: [{ + id: `${orgDid}#key-1`, + type: 'JsonWebKey2020', + controller: orgDid, + publicKeyJwk: pubJwk, + }], + authentication: [`${orgDid}#key-1`], + assertionMethod: [`${orgDid}#key-1`], + service: [{ + id: `${orgDid}#trail-registry`, + type: 'TrailRegistryService', + serviceEndpoint: 'https://trailprotocol.org/verify/ras-crew/', + }], +}; + +// Registry body (without proof) +const body = { + '@context': 'https://trailprotocol.org/ns/registry/v1', + name: 'RAS Crew PoC Registry', + description: 'First production reference for did:trail - the Rocking.AI.Sales AI Sales Crew (1 org + 5 agents).', + version: '0.1.0', + created: new Date().toISOString(), + notice: 'POC-KEY-NOT-FOR-PRODUCTION. All identifiers in this registry are signed with a single ed25519 test key. Do not trust for real transactions.', + signingKey: { + id: 'ras-crew-poc', + type: 'Ed25519VerificationKey2020', + publicKeyMultibase: pubMultibase, + publicKeyJwk: pubJwk, + }, + didDocuments: [orgDoc, ...agentDocs], +}; + +// Sign canonical body with the same ed25519 key +const canonical = canonicalize(body); +const sig = crypto.sign(null, Buffer.from(canonical), privateKey); +const proof = { + type: 'Ed25519Signature2020', + created: new Date().toISOString(), + verificationMethod: 'ras-crew-poc', + proofPurpose: 'assertionMethod', + proofValue: 'z' + base58btcEncode(sig), +}; + +const registry = { ...body, proof }; + +// ---------- write files ---------- +const repoRoot = path.resolve(__dirname, '..', '..', '..'); // trail-did-method/ +const examplesDir = path.join(repoRoot, 'examples'); +const registryPath = path.join(examplesDir, 'ras-crew-registry.json'); +const pubKeyPath = path.join(examplesDir, 'keys', 'ras-crew-poc.pub'); +const privKeyPath = path.join(os.homedir(), '.config', 'ras-crew-poc-signing.json'); + +fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n'); + +fs.writeFileSync(pubKeyPath, JSON.stringify({ + id: 'ras-crew-poc', + type: 'Ed25519VerificationKey2020', + note: 'POC-KEY-NOT-FOR-PRODUCTION - RAS Crew did:trail PoC registry signing key', + created: new Date().toISOString().slice(0, 10), + publicKeyMultibase: pubMultibase, + publicKeyJwk: pubJwk, +}, null, 2) + '\n'); + +fs.mkdirSync(path.dirname(privKeyPath), { recursive: true }); +fs.writeFileSync(privKeyPath, JSON.stringify({ + id: 'ras-crew-poc', + note: 'PRIVATE KEY - NEVER COMMIT. Move to macOS Keychain (entry: ras-crew-poc-signing) when convenient.', + created: new Date().toISOString().slice(0, 10), + privateKeyPem: privateKey.export({ format: 'pem', type: 'pkcs8' }), + publicKeyMultibase: pubMultibase, +}, null, 2) + '\n', { mode: 0o600 }); + +console.log('=== RAS Crew PoC Registry built ==='); +console.log('Org DID:', orgDid); +agentDocs.forEach(d => console.log('Agent: ', d.id, '·', d._meta.displayName)); +console.log(); +console.log('Registry: ', registryPath); +console.log('Pub key: ', pubKeyPath); +console.log('Priv key: ', privKeyPath, '(gitignored, move to Keychain)'); +console.log('Proof: ', proof.proofValue.slice(0, 40) + '...'); diff --git a/examples/js/ras-crew-poc/build-verify-pages.js b/examples/js/ras-crew-poc/build-verify-pages.js new file mode 100644 index 0000000..76dd692 --- /dev/null +++ b/examples/js/ras-crew-poc/build-verify-pages.js @@ -0,0 +1,255 @@ +#!/usr/bin/env node +/** + * RAS Crew did:trail PoC - Verify Pages Generator + * + * Reads ras-crew-registry.json and generates static verify pages for + * trailprotocol.org/verify/ras-crew// plus an index page. + * + * Run: + * node build-verify-pages.js + */ + +const fs = require('fs'); +const path = require('path'); + +const REGISTRY = path.resolve(__dirname, '..', '..', 'ras-crew-registry.json'); +const OUT_ROOT = path.resolve(process.env.HOME, 'Developer', 'trail', 'trailprotocol-io', 'verify', 'ras-crew'); +const REGISTRY_GITHUB = 'https://github.com/trailprotocol/trail-did-method/blob/main/examples/ras-crew-registry.json'; +const SPEC_URL = 'https://github.com/trailprotocol/trail-did-method/blob/main/spec/did-method-trail-v1.md'; +const PR13_URL = 'https://github.com/trailprotocol/trail-did-method/pull/13'; +const CCG_URL = 'https://lists.w3.org/Archives/Public/public-credentials/2026Apr/'; + +const reg = JSON.parse(fs.readFileSync(REGISTRY, 'utf8')); + +// short id = slug without org prefix, matches serviceEndpoint path +function shortId(agent) { + return agent._meta.slug.replace(/^rocking-ai-sales-/, '').replace(/-01$/, ''); +} + +const SHARED_CSS = ` +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} +:root{--bg:#07071A;--bg2:#0A0A1E;--bg3:#1a1a28;--border:rgba(255,255,255,.125);--purple:#7c6ee0;--blue:#4ea8de;--green:#a6e3a1;--gold:#e0a43a;--red:#f38ba8;--text:#cdd6f4;--muted:#a6adc1;--dim:#585b70} +html{scroll-behavior:smooth} +body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.7} +a{color:var(--blue);text-decoration:none;transition:color .2s} +a:hover{color:var(--purple)} +nav{position:sticky;top:0;z-index:100;background:rgba(10,10,15,.95);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:1rem 2rem} +.nav-inner{max-width:900px;margin:0 auto;display:flex;align-items:center;justify-content:space-between} +.nav-logo{font-size:1.1rem;font-weight:800;color:var(--purple);display:flex;align-items:center;gap:8px} +.nav-logo span{color:var(--text);font-weight:400;font-size:.85rem} +.nav-links{display:flex;gap:1.5rem} +.nav-links a{font-size:.82rem;color:var(--muted);font-weight:500} +.nav-links a:hover{color:var(--text)} +main{max-width:900px;margin:0 auto;padding:3rem 2rem 5rem} +.badge{display:inline-flex;align-items:center;gap:6px;background:rgba(166,227,161,.1);border:1px solid rgba(166,227,161,.35);border-radius:20px;padding:5px 14px;font-size:.72rem;font-weight:700;color:var(--green);text-transform:uppercase;letter-spacing:.5px;margin-bottom:1.5rem} +.badge.gold{background:rgba(224,164,58,.1);border-color:rgba(224,164,58,.35);color:var(--gold)} +h1{font-size:clamp(1.8rem,4vw,2.6rem);font-weight:900;line-height:1.15;letter-spacing:-1px;margin-bottom:.8rem} +h1 .hl{background:linear-gradient(135deg,var(--purple),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text} +.lede{font-size:1rem;color:var(--muted);max-width:700px;margin-bottom:2.5rem} +.did-box{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:1.2rem 1.4rem;margin-bottom:2rem;font-family:'Geist Mono','SF Mono',Consolas,monospace;font-size:.82rem;color:var(--blue);word-break:break-all;overflow-wrap:anywhere} +.did-label{font-family:'Inter',sans-serif;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:var(--purple);margin-bottom:.5rem;display:block} +.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.2rem;margin-bottom:2.5rem} +.card{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:1.4rem} +.card h3{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;color:var(--purple);margin-bottom:.6rem} +.card p{font-size:.9rem;color:var(--text);line-height:1.6} +.card p.muted{color:var(--muted);font-size:.82rem} +.verify-status{background:var(--bg2);border:1px solid rgba(166,227,161,.35);border-radius:12px;padding:1.5rem;margin-bottom:2.5rem} +.verify-status h2{font-size:1.1rem;font-weight:700;color:var(--green);margin-bottom:1rem;display:flex;align-items:center;gap:8px} +.verify-status ul{list-style:none;padding:0} +.verify-status li{font-size:.88rem;color:var(--muted);padding:.35rem 0;display:flex;gap:10px} +.verify-status li::before{content:"✓";color:var(--green);font-weight:800} +.verify-status code{font-family:'Geist Mono',monospace;font-size:.78rem;color:var(--blue);background:#161622;padding:2px 6px;border-radius:4px} +.footer-notice{background:rgba(224,164,58,.06);border:1px solid rgba(224,164,58,.3);border-radius:10px;padding:1rem 1.2rem;margin-bottom:2.5rem;font-size:.82rem;color:var(--muted)} +.footer-notice strong{color:var(--gold)} +.links{display:flex;gap:1.2rem;flex-wrap:wrap;padding-top:1.5rem;border-top:1px solid var(--border);font-size:.85rem} +.agent-list{display:grid;gap:.8rem;margin-top:1.5rem} +.agent-row{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:1rem 1.3rem;display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap} +.agent-row:hover{border-color:var(--purple)} +.agent-row .name{font-weight:700;color:var(--text)} +.agent-row .role{font-size:.78rem;color:var(--muted)} +.agent-row .did{font-family:'Geist Mono',monospace;font-size:.72rem;color:var(--dim);word-break:break-all} +footer.site{border-top:1px solid var(--border);padding:2rem;text-align:center;font-size:.78rem;color:var(--dim)} +`; + +function navHTML() { + return ``; +} + +function footerHTML() { + return ``; +} + +function agentPage(doc) { + const short = shortId(doc); + const name = doc['trail:displayName']; + const desc = doc['trail:description']; + const oversight = doc['trail:humanOversight']; + const parent = doc['trail:parentOrganization']; + const risk = doc['trail:euAiActRiskClass']; + const svc = doc.service[0].serviceEndpoint; + return ` + + + + +${name} - Verifizierbarer AI-Agent - TRAIL Protocol + + + + + + + + + + +${navHTML()} +
+
Verifizierbarer AI-Agent
+

${name}
Rocking.AI.Sales

+

${desc}. Diese Seite zeigt die kryptographisch verifizierbare Identitaet dieses AI-Agenten nach der did:trail DID Method.

+ + Decentralized Identifier (DID) +
${doc.id}
+ +
+
+

Human Oversight

+

${oversight.name}

+

${oversight.role}
${oversight.email}

+
+
+

Controller

+

Rocking.AI.Sales

+

${parent}

+
+
+

EU AI Act Risk Class

+

${risk}

+

Klassifizierung per Art. 6 EU AI Act

+
+
+

Trail Mode

+

agent

+

Pro did:trail Spec v1 Section 4.1

+
+
+ +
+

Verifikations-Status

+
    +
  • DID Document geladen aus signierter Registry (ras-crew-registry.json)
  • +
  • Ed25519Signature2020 der Registry verifiziert
  • +
  • trail-hash per Spec Section 4.5.2 (SHA-256(slug + ":" + publicKeyMultibase)[0:16]) geprueft
  • +
  • TrailRegistryService Endpoint vorhanden (Spec Section 3.3.1)
  • +
+
+ + + + +
+${footerHTML()} + + +`; +} + +function indexPage(agents, orgDoc) { + const rows = agents.map(a => { + const short = shortId(a); + return ` +
+
${a['trail:displayName']}
+
${a['trail:description']}
+
+
${a.id}
+
`; + }).join('\n'); + + return ` + + + + +RAS Crew - Verifizierbare AI-Agenten - TRAIL Protocol + + + + + + + + + +${navHTML()} +
+
Erste did:trail Registry Live
+

RAS Crew -
verifizierbare AI-Agenten

+

Dies ist die erste Production-Referenz fuer die did:trail DID Method: die Rocking.AI.Sales AI Sales Crew - 1 Organisation und 5 AI-Agenten, alle kryptographisch verifizierbar nach W3C DID, spec-konform zu did:trail v1 und EU AI Act aligned.

+ + Organisation +
${orgDoc.id}
+ + Agenten (5) +
${rows}
+ +
+

Registry Status

+
    +
  • Signiert mit Ed25519Signature2020 (Key: ras-crew-poc)
  • +
  • 6 DID Documents, alle mit TrailRegistryService Endpoint (Spec Section 3.3.1)
  • +
  • trail-hash verifiziert (Spec Section 4.5.2)
  • +
  • Human Oversight: Christian Hommrich, Rocking.AI.Sales
  • +
+
+ + + + +
+${footerHTML()} + + +`; +} + +// ---------- write ---------- +const orgDoc = reg.didDocuments.find(d => d['trail:trailMode'] === 'org'); +const agents = reg.didDocuments.filter(d => d['trail:trailMode'] === 'agent'); + +fs.mkdirSync(OUT_ROOT, { recursive: true }); + +for (const a of agents) { + const short = shortId(a); + const dir = path.join(OUT_ROOT, short); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'index.html'), agentPage(a)); + console.log('wrote', path.join('verify/ras-crew', short, 'index.html')); +} + +fs.writeFileSync(path.join(OUT_ROOT, 'index.html'), indexPage(agents, orgDoc)); +console.log('wrote verify/ras-crew/index.html'); +console.log('\nDone. Output dir:', OUT_ROOT); diff --git a/examples/js/resolve-ras-crew.js b/examples/js/resolve-ras-crew.js new file mode 100644 index 0000000..0af4731 --- /dev/null +++ b/examples/js/resolve-ras-crew.js @@ -0,0 +1,216 @@ +#!/usr/bin/env node +/** + * RAS Crew did:trail PoC - Local Resolver + * + * Resolves did:trail identifiers from the local ras-crew-registry.json, + * verifies the registry's ed25519 proof, and returns the matching DID Document. + * + * Usage: + * node resolve-ras-crew.js did:trail:agent:rocking-ai-sales-recherche-01- + * node resolve-ras-crew.js --list + * node resolve-ras-crew.js --test (runs positive + negative tests) + * + * Spec refs: + * - §3 Resolution: resolver returns DID Document + metadata + * - §3.3.1 Registry Discovery: TrailRegistryService endpoint REQUIRED + * - §4.5.2 trail-hash verification + */ + +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +const REGISTRY_PATH = path.resolve(__dirname, '..', 'ras-crew-registry.json'); + +// ---------- base58btc decode (for multibase z-prefix) ---------- +const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; +function base58btcDecode(str) { + const bytes = [0]; + for (let i = 0; i < str.length; i++) { + const c = ALPHABET.indexOf(str[i]); + if (c < 0) throw new Error('Invalid base58btc char: ' + str[i]); + let carry = c; + for (let j = 0; j < bytes.length; j++) { + carry += bytes[j] * 58; + bytes[j] = carry & 0xff; + carry >>= 8; + } + while (carry > 0) { bytes.push(carry & 0xff); carry >>= 8; } + } + for (let k = 0; k < str.length && str[k] === '1'; k++) bytes.push(0); + return Buffer.from(bytes.reverse()); +} + +function multibaseToRawEd25519(mb) { + if (!mb.startsWith('z')) throw new Error('Not a base58btc multibase value: ' + mb); + const decoded = base58btcDecode(mb.slice(1)); + if (decoded[0] !== 0xed || decoded[1] !== 0x01) { + throw new Error('Not an Ed25519 multibase key (expected 0xed01 prefix)'); + } + return decoded.slice(2); +} + +function rawEd25519ToSpkiPem(raw32) { + // SPKI prefix for Ed25519: 302a300506032b6570032100 + const spki = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), raw32]); + const b64 = spki.toString('base64').match(/.{1,64}/g).join('\n'); + return `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----\n`; +} + +// ---------- canonical JSON (must match build-registry.js) ---------- +function canonicalize(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) return '[' + value.map(canonicalize).join(',') + ']'; + const keys = Object.keys(value).sort(); + return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}'; +} + +// ---------- trail-hash per §4.5.2 ---------- +function trailHash(slug, publicKeyMultibase) { + return crypto.createHash('sha256').update(slug + ':' + publicKeyMultibase).digest('hex').slice(0, 16); +} + +// ---------- load + verify registry ---------- +function loadRegistry(registryJsonOrPath) { + const raw = typeof registryJsonOrPath === 'string' + ? JSON.parse(fs.readFileSync(registryJsonOrPath, 'utf8')) + : registryJsonOrPath; + return raw; +} + +function verifyRegistryProof(registry) { + const { proof, ...body } = registry; + if (!proof) throw new Error('Registry has no proof'); + if (proof.verificationMethod !== registry.signingKey.id) { + throw new Error(`Proof references unknown signing key: ${proof.verificationMethod}`); + } + const pubRaw = multibaseToRawEd25519(registry.signingKey.publicKeyMultibase); + const pubKey = crypto.createPublicKey({ key: rawEd25519ToSpkiPem(pubRaw), format: 'pem' }); + const canonical = canonicalize(body); + const sig = base58btcDecode(proof.proofValue.slice(1)); + const ok = crypto.verify(null, Buffer.from(canonical), pubKey, sig); + if (!ok) throw new Error('Registry signature verification FAILED'); + return true; +} + +// ---------- resolve ---------- +function resolve(did, registryPath = REGISTRY_PATH) { + const registry = loadRegistry(registryPath); + verifyRegistryProof(registry); + const doc = registry.didDocuments.find(d => d.id === did); + if (!doc) { + return { + didDocument: null, + didResolutionMetadata: { error: 'notFound', contentType: 'application/did+ld+json' }, + didDocumentMetadata: {}, + }; + } + // Verify trail-hash matches slug + key (§4.5.2) + const slug = doc._meta?.slug || doc.id.split(':').slice(3).join(':').replace(/-[0-9a-f]{16}$/, ''); + const expectedHash = trailHash(slug, registry.signingKey.publicKeyMultibase); + const actualHash = doc.id.slice(-16); + if (expectedHash !== actualHash) { + throw new Error(`trail-hash mismatch for ${did}: expected ${expectedHash}, got ${actualHash}`); + } + return { + didDocument: doc, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + registrySource: 'local:ras-crew-registry.json', + registryVerified: true, + trailHashVerified: true, + }, + didDocumentMetadata: { trailMode: doc['trail:trailMode'] }, + }; +} + +// ---------- CLI ---------- +const arg = process.argv[2]; +if (!arg || arg === '--help' || arg === '-h') { + console.log('Usage:'); + console.log(' resolve-ras-crew.js '); + console.log(' resolve-ras-crew.js --list'); + console.log(' resolve-ras-crew.js --test'); + process.exit(arg ? 0 : 1); +} + +if (arg === '--list') { + const reg = loadRegistry(REGISTRY_PATH); + verifyRegistryProof(reg); + console.log('Registry verified. Contents:'); + reg.didDocuments.forEach(d => { + const tag = d['trail:trailMode'] === 'org' ? '[ORG] ' : '[AGENT]'; + console.log(` ${tag} ${d.id}${d['trail:displayName'] ? ' · ' + d['trail:displayName'] : ''}`); + }); + process.exit(0); +} + +if (arg === '--test') { + let pass = 0, fail = 0; + const reg = loadRegistry(REGISTRY_PATH); + + // Test 1: valid signature + try { + verifyRegistryProof(reg); + console.log('PASS registry signature verifies'); + pass++; + } catch (e) { + console.log('FAIL registry signature:', e.message); + fail++; + } + + // Test 2: resolve all 6 DIDs + for (const d of reg.didDocuments) { + try { + const r = resolve(d.id); + if (r.didDocument?.id === d.id) { + console.log(`PASS resolve ${d.id.slice(0, 50)}...`); + pass++; + } else { + console.log(`FAIL resolve ${d.id}: not found`); + fail++; + } + } catch (e) { + console.log(`FAIL resolve ${d.id}: ${e.message}`); + fail++; + } + } + + // Test 3: unknown DID returns notFound + const bogus = resolve('did:trail:agent:does-not-exist-0000000000000000'); + if (bogus.didResolutionMetadata.error === 'notFound') { + console.log('PASS unknown DID returns notFound'); + pass++; + } else { + console.log('FAIL unknown DID should return notFound'); + fail++; + } + + // Test 4: tampered registry fails verification + try { + const tampered = JSON.parse(JSON.stringify(reg)); + tampered.didDocuments[0]['trail:displayName'] = 'TAMPERED'; + verifyRegistryProof(tampered); + console.log('FAIL tampered registry should NOT verify'); + fail++; + } catch (e) { + console.log('PASS tampered registry rejected:', e.message); + pass++; + } + + console.log(`\n${pass} passed, ${fail} failed`); + process.exit(fail === 0 ? 0 : 1); +} + +// Resolve single DID +try { + const result = resolve(arg); + if (!result.didDocument) { + console.error('Not found:', arg); + process.exit(2); + } + console.log(JSON.stringify(result, null, 2)); +} catch (e) { + console.error('Resolution failed:', e.message); + process.exit(1); +} diff --git a/examples/keys/ras-crew-poc.pub b/examples/keys/ras-crew-poc.pub new file mode 100644 index 0000000..890d82b --- /dev/null +++ b/examples/keys/ras-crew-poc.pub @@ -0,0 +1,12 @@ +{ + "id": "ras-crew-poc", + "type": "Ed25519VerificationKey2020", + "note": "POC-KEY-NOT-FOR-PRODUCTION - RAS Crew did:trail PoC registry signing key", + "created": "2026-04-14", + "publicKeyMultibase": "z6MktDCM9689mJcCrFHTqQjrLXiPZnbxvu27YTN57QBBbLB1", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zGlOEMcSD21BwLZwSVsW47xipSs3UMOoAMhZCeLUBOA" + } +} diff --git a/examples/ras-crew-registry.json b/examples/ras-crew-registry.json new file mode 100644 index 0000000..17bf983 --- /dev/null +++ b/examples/ras-crew-registry.json @@ -0,0 +1,304 @@ +{ + "@context": "https://trailprotocol.org/ns/registry/v1", + "name": "RAS Crew PoC Registry", + "description": "First production reference for did:trail - the Rocking.AI.Sales AI Sales Crew (1 org + 5 agents).", + "version": "0.1.0", + "created": "2026-04-14T11:47:04.750Z", + "notice": "POC-KEY-NOT-FOR-PRODUCTION. All identifiers in this registry are signed with a single ed25519 test key. Do not trust for real transactions.", + "signingKey": { + "id": "ras-crew-poc", + "type": "Ed25519VerificationKey2020", + "publicKeyMultibase": "z6MktDCM9689mJcCrFHTqQjrLXiPZnbxvu27YTN57QBBbLB1", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zGlOEMcSD21BwLZwSVsW47xipSs3UMOoAMhZCeLUBOA" + } + }, + "didDocuments": [ + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "controller": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:trailMode": "org", + "trail:displayName": "Rocking.AI.Sales", + "trail:legalName": "Rocking.AI.Sales (pre-incorporation, Christian Hommrich)", + "trail:jurisdiction": "DE", + "verificationMethod": [ + { + "id": "did:trail:org:rocking-ai-sales-a1772c746cf0287a#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zGlOEMcSD21BwLZwSVsW47xipSs3UMOoAMhZCeLUBOA" + } + } + ], + "authentication": [ + "did:trail:org:rocking-ai-sales-a1772c746cf0287a#key-1" + ], + "assertionMethod": [ + "did:trail:org:rocking-ai-sales-a1772c746cf0287a#key-1" + ], + "service": [ + { + "id": "did:trail:org:rocking-ai-sales-a1772c746cf0287a#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": "https://trailprotocol.org/verify/ras-crew/" + } + ] + }, + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:agent:rocking-ai-sales-recherche-01-f07db931d45bb82b", + "controller": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:trailMode": "agent", + "trail:aiSystemType": "agent", + "trail:euAiActRiskClass": "minimal", + "trail:parentOrganization": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:displayName": "Recherche-Agent", + "trail:description": "Rocking.AI.Sales Crew - Research / ICP scoring", + "trail:humanOversight": { + "name": "Christian Hommrich", + "email": "christian.hommrich@rockingaisales.de", + "role": "Founder / Operator" + }, + "verificationMethod": [ + { + "id": "did:trail:agent:rocking-ai-sales-recherche-01-f07db931d45bb82b#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:agent:rocking-ai-sales-recherche-01-f07db931d45bb82b", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zGlOEMcSD21BwLZwSVsW47xipSs3UMOoAMhZCeLUBOA" + } + } + ], + "authentication": [ + "did:trail:agent:rocking-ai-sales-recherche-01-f07db931d45bb82b#key-1" + ], + "assertionMethod": [ + "did:trail:agent:rocking-ai-sales-recherche-01-f07db931d45bb82b#key-1" + ], + "service": [ + { + "id": "did:trail:agent:rocking-ai-sales-recherche-01-f07db931d45bb82b#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": "https://trailprotocol.org/verify/ras-crew/recherche-01/" + } + ], + "_meta": { + "slug": "rocking-ai-sales-recherche-01", + "displayName": "Recherche-Agent" + } + }, + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:agent:rocking-ai-sales-erstkontakt-01-1858b310e081441b", + "controller": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:trailMode": "agent", + "trail:aiSystemType": "agent", + "trail:euAiActRiskClass": "minimal", + "trail:parentOrganization": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:displayName": "Erstkontakt-Agent", + "trail:description": "Rocking.AI.Sales Crew - Outreach drafting", + "trail:humanOversight": { + "name": "Christian Hommrich", + "email": "christian.hommrich@rockingaisales.de", + "role": "Founder / Operator" + }, + "verificationMethod": [ + { + "id": "did:trail:agent:rocking-ai-sales-erstkontakt-01-1858b310e081441b#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:agent:rocking-ai-sales-erstkontakt-01-1858b310e081441b", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zGlOEMcSD21BwLZwSVsW47xipSs3UMOoAMhZCeLUBOA" + } + } + ], + "authentication": [ + "did:trail:agent:rocking-ai-sales-erstkontakt-01-1858b310e081441b#key-1" + ], + "assertionMethod": [ + "did:trail:agent:rocking-ai-sales-erstkontakt-01-1858b310e081441b#key-1" + ], + "service": [ + { + "id": "did:trail:agent:rocking-ai-sales-erstkontakt-01-1858b310e081441b#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": "https://trailprotocol.org/verify/ras-crew/erstkontakt-01/" + } + ], + "_meta": { + "slug": "rocking-ai-sales-erstkontakt-01", + "displayName": "Erstkontakt-Agent" + } + }, + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:agent:rocking-ai-sales-qualifizierung-01-d9af73ea505b82ea", + "controller": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:trailMode": "agent", + "trail:aiSystemType": "agent", + "trail:euAiActRiskClass": "minimal", + "trail:parentOrganization": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:displayName": "Qualifizierungs-Agent", + "trail:description": "Rocking.AI.Sales Crew - PRISM qualification", + "trail:humanOversight": { + "name": "Christian Hommrich", + "email": "christian.hommrich@rockingaisales.de", + "role": "Founder / Operator" + }, + "verificationMethod": [ + { + "id": "did:trail:agent:rocking-ai-sales-qualifizierung-01-d9af73ea505b82ea#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:agent:rocking-ai-sales-qualifizierung-01-d9af73ea505b82ea", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zGlOEMcSD21BwLZwSVsW47xipSs3UMOoAMhZCeLUBOA" + } + } + ], + "authentication": [ + "did:trail:agent:rocking-ai-sales-qualifizierung-01-d9af73ea505b82ea#key-1" + ], + "assertionMethod": [ + "did:trail:agent:rocking-ai-sales-qualifizierung-01-d9af73ea505b82ea#key-1" + ], + "service": [ + { + "id": "did:trail:agent:rocking-ai-sales-qualifizierung-01-d9af73ea505b82ea#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": "https://trailprotocol.org/verify/ras-crew/qualifizierung-01/" + } + ], + "_meta": { + "slug": "rocking-ai-sales-qualifizierung-01", + "displayName": "Qualifizierungs-Agent" + } + }, + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:agent:rocking-ai-sales-vorbereitung-01-2efdf3ee9df8b85b", + "controller": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:trailMode": "agent", + "trail:aiSystemType": "agent", + "trail:euAiActRiskClass": "minimal", + "trail:parentOrganization": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:displayName": "Vorbereitungs-Agent", + "trail:description": "Rocking.AI.Sales Crew - Meeting prep", + "trail:humanOversight": { + "name": "Christian Hommrich", + "email": "christian.hommrich@rockingaisales.de", + "role": "Founder / Operator" + }, + "verificationMethod": [ + { + "id": "did:trail:agent:rocking-ai-sales-vorbereitung-01-2efdf3ee9df8b85b#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:agent:rocking-ai-sales-vorbereitung-01-2efdf3ee9df8b85b", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zGlOEMcSD21BwLZwSVsW47xipSs3UMOoAMhZCeLUBOA" + } + } + ], + "authentication": [ + "did:trail:agent:rocking-ai-sales-vorbereitung-01-2efdf3ee9df8b85b#key-1" + ], + "assertionMethod": [ + "did:trail:agent:rocking-ai-sales-vorbereitung-01-2efdf3ee9df8b85b#key-1" + ], + "service": [ + { + "id": "did:trail:agent:rocking-ai-sales-vorbereitung-01-2efdf3ee9df8b85b#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": "https://trailprotocol.org/verify/ras-crew/vorbereitung-01/" + } + ], + "_meta": { + "slug": "rocking-ai-sales-vorbereitung-01", + "displayName": "Vorbereitungs-Agent" + } + }, + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:agent:rocking-ai-sales-nachfass-01-80bae7205b9ec964", + "controller": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:trailMode": "agent", + "trail:aiSystemType": "agent", + "trail:euAiActRiskClass": "minimal", + "trail:parentOrganization": "did:trail:org:rocking-ai-sales-a1772c746cf0287a", + "trail:displayName": "Nachfass-Agent", + "trail:description": "Rocking.AI.Sales Crew - Follow-up / action items", + "trail:humanOversight": { + "name": "Christian Hommrich", + "email": "christian.hommrich@rockingaisales.de", + "role": "Founder / Operator" + }, + "verificationMethod": [ + { + "id": "did:trail:agent:rocking-ai-sales-nachfass-01-80bae7205b9ec964#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:agent:rocking-ai-sales-nachfass-01-80bae7205b9ec964", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zGlOEMcSD21BwLZwSVsW47xipSs3UMOoAMhZCeLUBOA" + } + } + ], + "authentication": [ + "did:trail:agent:rocking-ai-sales-nachfass-01-80bae7205b9ec964#key-1" + ], + "assertionMethod": [ + "did:trail:agent:rocking-ai-sales-nachfass-01-80bae7205b9ec964#key-1" + ], + "service": [ + { + "id": "did:trail:agent:rocking-ai-sales-nachfass-01-80bae7205b9ec964#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": "https://trailprotocol.org/verify/ras-crew/nachfass-01/" + } + ], + "_meta": { + "slug": "rocking-ai-sales-nachfass-01", + "displayName": "Nachfass-Agent" + } + } + ], + "proof": { + "type": "Ed25519Signature2020", + "created": "2026-04-14T11:47:04.758Z", + "verificationMethod": "ras-crew-poc", + "proofPurpose": "assertionMethod", + "proofValue": "zTgbvM2SG8qNRgV3zmfmq5fdWGsdx1o56oMV4feew35ypB96dhFXQkWuvsqESLNDvrsQ3hrQDLjYfjQQm4HqTbBq" + } +}