Skip to content

Commit 0a6beaf

Browse files
committed
feat(trust): WAB v1.3 cryptographic trust layer (Ed25519 + DNSSEC + signed manifests)
1 parent 3765cab commit 0a6beaf

9 files changed

Lines changed: 992 additions & 1 deletion

File tree

examples/wab-sign.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env node
2+
/**
3+
* wab-sign — generate Ed25519 keys and sign WAB discovery manifests.
4+
*
5+
* Usage:
6+
* wab-sign keygen # print a new keypair (save private offline)
7+
* wab-sign sign manifest.json key.priv # sign a manifest, write manifest.signed.json
8+
* wab-sign txt <pubkey-b64> <endpoint> # print the matching _wab TXT line
9+
*
10+
* Examples:
11+
* $ node wab-sign.js keygen > keys.json
12+
* $ jq -r .private_key keys.json > key.priv
13+
* $ node wab-sign.js sign wab.json key.priv
14+
*
15+
* $ node wab-sign.js txt <(jq -r .public_key keys.json) https://example.com/.well-known/wab.json
16+
*/
17+
18+
'use strict';
19+
20+
const fs = require('fs');
21+
const path = require('path');
22+
const { generateKeyPair, signManifest, fingerprint } = require(
23+
// try the bundled service first; fall back to a local copy if invoked from a downloads/ extract
24+
fs.existsSync(path.join(__dirname, '..', 'server', 'services', 'wab-crypto.js'))
25+
? path.join(__dirname, '..', 'server', 'services', 'wab-crypto.js')
26+
: './wab-crypto'
27+
);
28+
29+
const [,, cmd, a1, a2] = process.argv;
30+
31+
function usage() {
32+
console.error('Usage:');
33+
console.error(' wab-sign keygen');
34+
console.error(' wab-sign sign <manifest.json> <key.priv>');
35+
console.error(' wab-sign txt <public-key-b64> <endpoint-url>');
36+
process.exit(1);
37+
}
38+
39+
if (!cmd) usage();
40+
41+
if (cmd === 'keygen') {
42+
const kp = generateKeyPair();
43+
process.stdout.write(JSON.stringify(kp, null, 2) + '\n');
44+
process.stderr.write('\n[!] private_key is shown ONLY here. Save it offline immediately.\n');
45+
process.stderr.write('[!] Publish public_key in your _wab DNS TXT as: pk=ed25519:' + kp.public_key + '\n');
46+
process.exit(0);
47+
}
48+
49+
if (cmd === 'sign') {
50+
if (!a1 || !a2) usage();
51+
const manifest = JSON.parse(fs.readFileSync(a1, 'utf8'));
52+
const priv = fs.readFileSync(a2, 'utf8').trim();
53+
const signed = signManifest(manifest, priv);
54+
const out = a1.replace(/\.json$/, '') + '.signed.json';
55+
fs.writeFileSync(out, JSON.stringify(signed, null, 2) + '\n');
56+
console.log(`[OK] Signed manifest written: ${out}`);
57+
console.log(`[OK] Signature key_id: ${signed.signature.key_id}`);
58+
console.log(`[OK] Upload ${out} to https://${manifest.domain || '<your-domain>'}/.well-known/wab.json`);
59+
process.exit(0);
60+
}
61+
62+
if (cmd === 'txt') {
63+
if (!a1 || !a2) usage();
64+
const pub = a1.trim();
65+
const endpoint = a2.trim();
66+
if (!/^https:\/\//i.test(endpoint)) { console.error('endpoint must be HTTPS'); process.exit(1); }
67+
const fp = fingerprint(pub);
68+
console.log(`# _wab.${endpoint.replace(/^https?:\/\//,'').replace(/\/.*$/,'')} TXT record:`);
69+
console.log(`v=wab1; endpoint=${endpoint}; pk=ed25519:${pub}`);
70+
console.log(`# key_id (fingerprint): ${fp}`);
71+
process.exit(0);
72+
}
73+
74+
usage();

examples/wab-verify.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env node
2+
/**
3+
* wab-verify — full trust check on a domain's WAB DNS Discovery setup.
4+
*
5+
* Usage:
6+
* wab-verify <domain> # full DNS + DNSSEC + signature trust report
7+
* wab-verify --json <domain> # JSON output (for CI / scripts)
8+
* wab-verify --strict <domain> # exit non-zero unless trust_score == 100
9+
*
10+
* Hits the public WAB Trust API at /api/discovery/trust/:domain.
11+
* Override base URL with WAB_BASE_URL=https://your-instance.example.com
12+
*/
13+
14+
'use strict';
15+
16+
const fetch = (() => { try { return require('node-fetch'); } catch { return globalThis.fetch; } })();
17+
18+
const args = process.argv.slice(2);
19+
const json = args.includes('--json');
20+
const strict = args.includes('--strict');
21+
const domain = args.find(a => !a.startsWith('--'));
22+
const BASE = process.env.WAB_BASE_URL || 'https://www.webagentbridge.com';
23+
24+
if (!domain) {
25+
console.error('Usage: wab-verify [--json] [--strict] <domain>');
26+
process.exit(2);
27+
}
28+
29+
(async () => {
30+
const r = await fetch(`${BASE}/api/discovery/trust/${encodeURIComponent(domain)}`);
31+
if (!r.ok) { console.error(`HTTP ${r.status}`); process.exit(2); }
32+
const data = await r.json();
33+
34+
if (json) {
35+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
36+
} else {
37+
const c = data.checks || {};
38+
const tick = (b) => b ? '✓' : '✗';
39+
console.log(`\nWAB Trust Report — ${data.domain}`);
40+
console.log('═'.repeat(50));
41+
console.log(` Trust Score: ${data.trust_score}/100 [${data.trust_label.toUpperCase()}]`);
42+
console.log(' ──────────────────────────────────────────────');
43+
console.log(` ${tick(c.dns_resolved)} DNS _wab record present`);
44+
console.log(` ${tick(c.dnssec_verified)} DNSSEC verified (AD flag)`);
45+
console.log(` ${tick(c.has_public_key)} Public key in DNS (pk=${c.pk_algorithm || '—'})`);
46+
console.log(` ${tick(c.https_endpoint && c.manifest_fetched)} HTTPS manifest reachable`);
47+
console.log(` ${tick(c.signature_valid)} Manifest signature valid`);
48+
console.log(' ──────────────────────────────────────────────');
49+
if (data.public_key) console.log(` Key ID: ${data.public_key.fingerprint} (ed25519)`);
50+
if (data.endpoint) console.log(` Endpoint: ${data.endpoint}`);
51+
if (data.findings && data.findings.length) {
52+
console.log('\n Findings:');
53+
for (const f of data.findings) console.log(' • ' + f);
54+
}
55+
console.log('');
56+
}
57+
58+
if (strict && data.trust_score < 100) process.exit(1);
59+
process.exit(0);
60+
})().catch(err => { console.error('[ERROR]', err.message); process.exit(2); });

public/adoption-metrics.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ <h3>Recent runs</h3>
7878

7979
<div class="panel" style="text-align:center;color:#9eb1d8;font-size:.86rem;">
8080
Want your domain on this dashboard? Run any <a href="/dns">WAB DNS</a> usage proof —
81+
verify cryptographic trust at <a href="/wab-trust">/wab-trust</a>
8182
or one-click enable via
8283
<a href="/cloudflare-integration">Cloudflare</a>,
8384
<a href="/route53-integration">Route 53</a>,

public/provider-onboarding.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ <h3>Integration Endpoints</h3>
160160
<a class="btn-mini btn-primary" href="/azure-dns-integration">Azure DNS One-Click</a>
161161
<a class="btn-mini btn-primary" href="/registrar-integrations">GoDaddy / Namecheap CLI</a>
162162
<a class="btn-mini btn-secondary" href="/adoption-metrics">Adoption Metrics</a>
163+
<a class="btn-mini btn-primary" href="/wab-trust">Trust Layer (v1.3)</a>
164+
<a class="btn-mini btn-secondary" href="/wab-vs-protocols">WAB vs Other Protocols</a>
163165
<a class="btn-mini btn-secondary" href="/downloads/wab-dns-provider.postman_collection.json" target="_blank" rel="noopener">Download Postman Collection</a>
164166
<a class="btn-mini btn-secondary" href="/dns">Open DNS Page</a>
165167
</div>

public/wab-trust.html

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>WAB Trust — Cryptographic Verification (v1.3)</title>
7+
<meta name="description" content="WAB DNS Discovery v1.3 trust layer: DNSSEC + Ed25519 signed manifests anchored in domain ownership. Verify any domain's cryptographic trust score live.">
8+
<link rel="stylesheet" href="/css/styles.css?v=3.3.0">
9+
<style>
10+
body { background:#081123; color:#e8efff; }
11+
.hero { padding:100px 20px 26px; text-align:center; }
12+
.hero .badge { display:inline-block; background:linear-gradient(135deg,#10b981,#059669); padding:4px 12px; border-radius:999px; font-size:.78rem; font-weight:700; letter-spacing:.5px; margin-bottom:12px; }
13+
.hero h1 { font-size:clamp(1.9rem,4vw,2.8rem); margin:6px 0 8px; }
14+
.hero p { color:#9eb1d8; max-width:840px; margin:0 auto; font-size:1rem; }
15+
.wrap { max-width:1180px; margin:0 auto; padding:0 18px 70px; }
16+
.panel { background:linear-gradient(180deg,rgba(17,24,39,.92),rgba(10,16,30,.96)); border:1px solid rgba(148,163,184,.2); border-radius:14px; padding:18px; margin-bottom:16px; }
17+
.panel h3 { margin:0 0 10px; color:#fcd34d; font-size:1rem; }
18+
.grid2 { display:grid; grid-template-columns:1.2fr 1fr; gap:14px; }
19+
@media (max-width:880px) { .grid2 { grid-template-columns:1fr; } }
20+
label { display:block; font-size:.78rem; color:#a8b7d7; margin-bottom:5px; }
21+
input, textarea, select { width:100%; background:#0b162a; border:1px solid rgba(148,163,184,.28); color:#e8efff; border-radius:10px; padding:10px; font-family:Consolas,monospace; font-size:.85rem; }
22+
button { background:linear-gradient(135deg,#10b981,#047857); color:#fff; border:none; border-radius:10px; padding:10px 14px; cursor:pointer; font-weight:700; font-size:.85rem; }
23+
button.alt { background:linear-gradient(135deg,#334155,#1f2937); }
24+
button.warn { background:linear-gradient(135deg,#f59e0b,#b45309); }
25+
.actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
26+
pre { background:#020617; border:1px solid rgba(148,163,184,.26); border-radius:10px; padding:12px; color:#c4b5fd; overflow:auto; font-size:.78rem; }
27+
.score-card { text-align:center; padding:20px; border-radius:14px; }
28+
.score-num { font-size:3.4rem; font-weight:800; line-height:1; }
29+
.score-label { font-size:.86rem; text-transform:uppercase; letter-spacing:1.4px; margin-top:6px; }
30+
.platinum { background:linear-gradient(135deg,#a855f7,#6366f1); color:#fff; }
31+
.gold { background:linear-gradient(135deg,#fbbf24,#d97706); color:#1f2937; }
32+
.silver { background:linear-gradient(135deg,#cbd5e1,#94a3b8); color:#1f2937; }
33+
.bronze { background:linear-gradient(135deg,#f97316,#c2410c); color:#fff; }
34+
.basic { background:linear-gradient(135deg,#475569,#1f2937); color:#fff; }
35+
.unverified { background:#1f2937; color:#a8b7d7; border:1px solid rgba(148,163,184,.26); }
36+
.check-row { display:flex; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid rgba(148,163,184,.12); font-size:.9rem; }
37+
.check-row:last-child { border-bottom:0; }
38+
.check-icon { width:22px; height:22px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:.78rem; }
39+
.check-icon.ok { background:#064e3b; color:#a7f3d0; }
40+
.check-icon.no { background:#7f1d1d; color:#fecaca; }
41+
.findings { background:#0b162a; border-left:3px solid #f59e0b; padding:10px 14px; border-radius:6px; font-size:.84rem; color:#fde68a; margin-top:10px; }
42+
.findings ul { margin:6px 0 0 18px; padding:0; }
43+
table { width:100%; border-collapse:collapse; font-size:.84rem; }
44+
th, td { padding:8px 10px; text-align:left; border-bottom:1px solid rgba(148,163,184,.14); }
45+
th { color:#a8b7d7; font-weight:600; font-size:.74rem; text-transform:uppercase; letter-spacing:.5px; }
46+
.pill { display:inline-block; padding:2px 8px; border-radius:999px; font-size:.7rem; font-weight:700; }
47+
.pill.ok { background:#064e3b; color:#a7f3d0; }
48+
.pill.no { background:#7f1d1d; color:#fecaca; }
49+
.keyout { display:none; }
50+
a { color:#fcd34d; }
51+
.small { font-size:.8rem; color:#9eb1d8; }
52+
</style>
53+
</head>
54+
<body>
55+
<section class="hero">
56+
<span class="badge">WAB v1.3 · Trust Layer</span>
57+
<h1>Cryptographic Trust for Agent Discovery</h1>
58+
<p>WAB v1.3 binds every domain's discovery manifest to an <strong>Ed25519 signature</strong>, with the public key published in DNS and protected by <strong>DNSSEC</strong>. No CA. No central registry. Trust is rooted exactly where it should be — in domain ownership.</p>
59+
</section>
60+
61+
<div class="wrap">
62+
63+
<div class="grid2">
64+
<!-- LIVE VERIFIER -->
65+
<div class="panel">
66+
<h3>Verify any domain</h3>
67+
<p class="small">Runs the full 5-check trust report: DNS · DNSSEC · public key · HTTPS manifest · signature.</p>
68+
<label>Domain</label>
69+
<input id="vDomain" placeholder="example.com" value="webagentbridge.com" />
70+
<div class="actions">
71+
<button onclick="runVerify()">Run Trust Check</button>
72+
<button class="alt" onclick="loadLeaderboard()">View Leaderboard</button>
73+
</div>
74+
<div id="vResult" style="margin-top:14px;"></div>
75+
</div>
76+
77+
<!-- KEY GENERATOR -->
78+
<div class="panel">
79+
<h3>Generate Ed25519 keypair</h3>
80+
<p class="small">Stateless. The server never stores your private key — save it offline and use it to sign your <code>wab.json</code>.</p>
81+
<div class="actions">
82+
<button class="warn" onclick="genKeys()">Generate New Keypair</button>
83+
<button class="alt" onclick="document.getElementById('keyout').style.display='none'">Hide</button>
84+
</div>
85+
<div id="keyout" class="keyout" style="margin-top:14px;">
86+
<label>TXT record (publish in DNS)</label>
87+
<pre id="kTxt"></pre>
88+
<label>Public key (base64)</label>
89+
<pre id="kPub"></pre>
90+
<label>Private key (base64) — save offline NOW</label>
91+
<pre id="kPriv" style="border-color:#7f1d1d;color:#fecaca;"></pre>
92+
<p class="small" style="color:#fde68a;">⚠ This is the only time the private key is shown. Copy it now.</p>
93+
</div>
94+
</div>
95+
</div>
96+
97+
<!-- LEADERBOARD -->
98+
<div class="panel" id="leaderboardPanel" style="display:none;">
99+
<h3>Trust Leaderboard</h3>
100+
<div id="leaderboard">Loading…</div>
101+
</div>
102+
103+
<!-- HOW IT WORKS -->
104+
<div class="panel">
105+
<h3>How WAB v1.3 trust works</h3>
106+
<ol style="color:#cbd5e1;font-size:.9rem;line-height:1.7;padding-left:20px;">
107+
<li><strong>Generate keypair</strong> (Ed25519, 32 bytes each) — offline or via the button above.</li>
108+
<li><strong>Publish public key in DNS</strong>: <code>_wab IN TXT "v=wab1; endpoint=https://example.com/.well-known/wab.json; pk=ed25519:&lt;BASE64&gt;"</code>.</li>
109+
<li><strong>Enable DNSSEC</strong> on your zone (most registrars: 1-click). DNSSEC chain protects the key from spoofing.</li>
110+
<li><strong>Sign your manifest</strong> with the private key: <code>node wab-sign.js sign wab.json key.priv</code><code>wab.signed.json</code>.</li>
111+
<li><strong>Upload signed manifest</strong> to <code>https://example.com/.well-known/wab.json</code>.</li>
112+
<li>Agents fetch the TXT (verify DNSSEC) → fetch the manifest (verify signature with DNS-published key) → trust established.</li>
113+
</ol>
114+
<p class="small" style="margin-top:10px;">Compare to ANS, sitemap.xml, llms.txt, ai.txt:
115+
<a href="/wab-vs-protocols">WAB vs other protocols →</a></p>
116+
<p class="small">Offline tools: <a href="/examples/wab-sign.js" target="_blank">wab-sign.js</a> · <a href="/examples/wab-verify.js" target="_blank">wab-verify.js</a></p>
117+
</div>
118+
119+
</div>
120+
121+
<script>
122+
const tick = (b) => b ? '<div class="check-icon ok">✓</div>' : '<div class="check-icon no">✗</div>';
123+
124+
async function runVerify() {
125+
const d = document.getElementById('vDomain').value.trim();
126+
if (!d) return;
127+
const out = document.getElementById('vResult');
128+
out.innerHTML = '<p class="small">Running trust check…</p>';
129+
try {
130+
const r = await fetch(`/api/discovery/trust/${encodeURIComponent(d)}`);
131+
const data = await r.json();
132+
if (!r.ok) { out.innerHTML = `<p style="color:#fecaca;">Error: ${data.error || r.status}</p>`; return; }
133+
const c = data.checks || {};
134+
const cls = data.trust_label || 'unverified';
135+
out.innerHTML = `
136+
<div class="score-card ${cls}">
137+
<div class="score-num">${data.trust_score}<span style="font-size:1.2rem;opacity:.7">/100</span></div>
138+
<div class="score-label">${cls}</div>
139+
</div>
140+
<div style="margin-top:10px;">
141+
<div class="check-row">${tick(c.dns_resolved)}<span>DNS <code>_wab</code> record present</span></div>
142+
<div class="check-row">${tick(c.dnssec_verified)}<span>DNSSEC verified (AD flag set)</span></div>
143+
<div class="check-row">${tick(c.has_public_key)}<span>Public key in DNS (<code>pk=${c.pk_algorithm || '—'}</code>)</span></div>
144+
<div class="check-row">${tick(c.https_endpoint && c.manifest_fetched)}<span>HTTPS manifest reachable</span></div>
145+
<div class="check-row">${tick(c.signature_valid)}<span>Manifest signature valid (Ed25519)</span></div>
146+
</div>
147+
${data.public_key ? `<p class="small" style="margin-top:10px;">Key fingerprint: <code>${data.public_key.fingerprint}</code></p>` : ''}
148+
${data.findings && data.findings.length ? `<div class="findings"><strong>Findings:</strong><ul>${data.findings.map(f=>`<li>${f}</li>`).join('')}</ul></div>` : ''}
149+
`;
150+
} catch (err) {
151+
out.innerHTML = `<p style="color:#fecaca;">Error: ${err.message}</p>`;
152+
}
153+
}
154+
155+
async function genKeys() {
156+
try {
157+
const r = await fetch('/api/discovery/keys/generate', { method:'POST' });
158+
const k = await r.json();
159+
document.getElementById('keyout').style.display = 'block';
160+
document.getElementById('kTxt').textContent = k.txt_record_snippet;
161+
document.getElementById('kPub').textContent = k.public_key;
162+
document.getElementById('kPriv').textContent = k.private_key;
163+
} catch (err) {
164+
alert('Key generation failed: ' + err.message);
165+
}
166+
}
167+
168+
async function loadLeaderboard() {
169+
const panel = document.getElementById('leaderboardPanel');
170+
panel.style.display = 'block';
171+
const out = document.getElementById('leaderboard');
172+
try {
173+
const r = await fetch('/api/discovery/trust-leaderboard');
174+
const data = await r.json();
175+
if (!data.domains || !data.domains.length) {
176+
out.innerHTML = '<p class="small">No trust runs recorded yet. Run a verify to populate.</p>';
177+
return;
178+
}
179+
out.innerHTML = `<table>
180+
<thead><tr><th>#</th><th>Domain</th><th>Score</th><th>DNSSEC</th><th>pk</th><th>Signed</th><th>Sig OK</th><th>Last check</th></tr></thead>
181+
<tbody>${data.domains.map((d,i)=>`
182+
<tr>
183+
<td>${i+1}</td>
184+
<td>${d.domain}</td>
185+
<td><strong>${d.score}</strong></td>
186+
<td>${d.dnssec === 'verified' ? '<span class="pill ok">✓</span>' : '<span class="pill no">'+d.dnssec+'</span>'}</td>
187+
<td>${d.has_pk ? '<span class="pill ok">✓</span>' : '<span class="pill no">—</span>'}</td>
188+
<td>${d.signed_manifest ? '<span class="pill ok">✓</span>' : '<span class="pill no">—</span>'}</td>
189+
<td>${d.signature_valid ? '<span class="pill ok">✓</span>' : '<span class="pill no">—</span>'}</td>
190+
<td class="small">${(d.last_checked||'').replace('T',' ').slice(0,16)}</td>
191+
</tr>
192+
`).join('')}</tbody>
193+
</table>`;
194+
} catch (err) {
195+
out.innerHTML = `<p style="color:#fecaca;">Error: ${err.message}</p>`;
196+
}
197+
}
198+
</script>
199+
</body>
200+
</html>

0 commit comments

Comments
 (0)