-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify.js
More file actions
78 lines (68 loc) · 3.31 KB
/
Copy pathverify.js
File metadata and controls
78 lines (68 loc) · 3.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// verify.js — in-browser Ed25519 verification of Dynamic Feed signed envelopes.
//
// Adapted from the official shared verifier (https://dynamicfeed.ai/verify.js) so it
// runs on ANY origin: the JWKS fetch uses an absolute URL instead of a relative one.
// Canonicalization recipe ("json-sorted-compact"): https://dynamicfeed.ai/standard
//
// The only dependency is @noble/ed25519 — audited, ~4 kB, loaded from jsdelivr ESM.
import * as ed from 'https://cdn.jsdelivr.net/npm/@noble/ed25519@2/+esm';
import { lparse, canon, field } from './canon.js';
const DF = 'https://dynamicfeed.ai';
// Always-true parametric attest (Sydney air temp ≥ −50 °C) → an always-fresh signed sample.
export const ATTEST_URL =
`${DF}/v1/attest?metric=temperature_c&op=gte&threshold=-50&lat=-33.87&lon=151.21`;
function b64u(s) {
s = s.replace(/-/g, '+').replace(/_/g, '/');
s += '='.repeat((4 - s.length % 4) % 4);
const bin = atob(s), u = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i);
return u;
}
let _jwks = null;
async function jwks() {
if (_jwks) return _jwks;
_jwks = await (await fetch(`${DF}/.well-known/keys`)).json();
return _jwks;
}
/**
* Verify a signed Dynamic Feed response (raw JSON text — do NOT JSON.parse it first;
* a parse/re-stringify loses float repr and breaks the byte-exact canonical form).
* Returns { ok, error?, keyId, alg, canon }.
*/
export async function verify(text) {
let root;
try { root = lparse(String(text).trim()); }
catch (e) { return { ok: false, error: 'invalid JSON — ' + e.message }; }
if (root.t !== 'o') return { ok: false, error: 'expected a JSON object' };
const sig = field(root, 'signature');
if (!sig) return { ok: false, error: 'no "signature" block in this response' };
const keyId = (field(sig, 'key_id') || {}).v;
const sigB64 = (field(sig, 'sig') || {}).v;
const alg = (field(sig, 'alg') || {}).v;
const canonName = (field(sig, 'canonicalization') || {}).v;
if (!keyId || !sigB64) return { ok: false, error: 'signature block missing key_id or sig' };
let ks;
try { ks = await jwks(); }
catch { return { ok: false, error: 'could not fetch the public key (' + DF + '/.well-known/keys)' }; }
if (!(keyId in ks)) return { ok: false, error: 'key_id ' + keyId + ' not in published JWKS (rotated?)', keyId };
// Strip the signature, re-canonicalize byte-for-byte, verify the detached signature.
const stripped = { t: 'o', v: root.v.filter(p => p[0] !== 'signature') };
const msg = new TextEncoder().encode(canon(stripped));
let ok = false;
try { ok = await ed.verifyAsync(b64u(sigB64), msg, b64u(ks[keyId])); }
catch (e) { return { ok: false, error: 'verify error — ' + e.message, keyId }; }
return { ok, keyId, alg, canon: canonName };
}
/** Fetch a fresh signed attestation (raw text — keep it raw for verification). */
export async function fetchAttestation() {
const r = await fetch(ATTEST_URL);
if (!r.ok) throw new Error('attest endpoint responded ' + r.status);
return await r.text();
}
/** Flip one data digit (before the signature block) to demonstrate tamper-evidence. */
export function tamper(text) {
const m = text.match(/(\d)(?=[\s\S]*"signature")/);
if (!m) return text;
const idx = text.indexOf(m[0]);
return text.slice(0, idx) + String((Number(m[0]) + 1) % 10) + text.slice(idx + 1);
}