insumer-verify 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/index.d.ts +13 -5
- package/build/index.js +177 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Client-side verifier for [InsumerAPI](https://insumermodel.com/developers/) atte
|
|
|
4
4
|
|
|
5
5
|
**In production:** [DJD Agent Score](https://github.com/jacobsd32-cpu/djdagentscore) (Coinbase x402 ecosystem) uses insumer-verify for client-side cryptographic verification in their AI agent wallet trust scoring pipeline. [Case study](https://insumermodel.com/blog/djd-agent-score-insumer-api-integration.html).
|
|
6
6
|
|
|
7
|
-
Part of the InsumerAPI ecosystem: [REST API](https://insumermodel.com/developers/) (25 endpoints,
|
|
7
|
+
Part of the InsumerAPI ecosystem: [REST API](https://insumermodel.com/developers/) (25 endpoints, 32 chains) | [MCP server](https://www.npmjs.com/package/mcp-server-insumer) (npm) | [LangChain](https://pypi.org/project/langchain-insumer/) (PyPI) | [OpenAI GPT](https://chatgpt.com/g/g-699c5e43ce2481918b3f1e7f144c8a49-insumerapi-verify) (GPT Store)
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
package/build/index.d.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Validates ECDSA P-256 signatures, condition hashes, block freshness,
|
|
5
5
|
* and attestation expiry using the Web Crypto API. Zero dependencies.
|
|
6
6
|
* Works in Node.js 18+ and modern browsers.
|
|
7
|
+
*
|
|
8
|
+
* Accepts two input formats (auto-detected):
|
|
9
|
+
* - **JWT string**: ES256-signed JWT from POST /v1/attest with format: "jwt"
|
|
10
|
+
* - **Object**: Raw API response object with data.attestation and data.sig
|
|
7
11
|
*/
|
|
8
12
|
export interface VerifyResult {
|
|
9
13
|
valid: boolean;
|
|
@@ -29,14 +33,18 @@ export interface VerifyOptions {
|
|
|
29
33
|
/**
|
|
30
34
|
* Verify an InsumerAPI attestation response.
|
|
31
35
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
36
|
+
* Auto-detects input format:
|
|
37
|
+
* - **String** → JWT verification path (ES256 signature via JWKS)
|
|
38
|
+
* - **Object** → Raw attestation response path (ECDSA P1363 signature)
|
|
39
|
+
*
|
|
40
|
+
* Both formats run the same 4 checks:
|
|
41
|
+
* 1. **Signature** — ECDSA P-256 verification
|
|
34
42
|
* 2. **Condition hashes** — SHA-256 of canonical sorted-key JSON
|
|
35
43
|
* 3. **Freshness** — blockTimestamp age vs caller-defined maxAge (optional)
|
|
36
|
-
* 4. **Expiry** — whether the
|
|
44
|
+
* 4. **Expiry** — whether the attestation window has elapsed
|
|
37
45
|
*
|
|
38
|
-
* @param response
|
|
39
|
-
* @param options Optional configuration: maxAge (seconds)
|
|
46
|
+
* @param response JWT string or full API response object
|
|
47
|
+
* @param options Optional configuration: maxAge (seconds), jwksUrl
|
|
40
48
|
* @returns Structured result with overall validity and per-check details
|
|
41
49
|
*/
|
|
42
50
|
export declare function verifyAttestation(response: unknown, options?: VerifyOptions): Promise<VerifyResult>;
|
package/build/index.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Validates ECDSA P-256 signatures, condition hashes, block freshness,
|
|
5
5
|
* and attestation expiry using the Web Crypto API. Zero dependencies.
|
|
6
6
|
* Works in Node.js 18+ and modern browsers.
|
|
7
|
+
*
|
|
8
|
+
* Accepts two input formats (auto-detected):
|
|
9
|
+
* - **JWT string**: ES256-signed JWT from POST /v1/attest with format: "jwt"
|
|
10
|
+
* - **Object**: Raw API response object with data.attestation and data.sig
|
|
7
11
|
*/
|
|
8
12
|
// ── Public key ─────────────────────────────────────────────────────
|
|
9
13
|
/**
|
|
@@ -17,6 +21,7 @@ const PUBLIC_KEY_JWK = {
|
|
|
17
21
|
y: "kn34HaxVSJfn8NxwNEBjjLkcrM_GDw1lgnqyADGuc4c",
|
|
18
22
|
crv: "P-256",
|
|
19
23
|
};
|
|
24
|
+
const DEFAULT_JWKS_URL = "https://insumermodel.com/.well-known/jwks.json";
|
|
20
25
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
21
26
|
const subtle = globalThis.crypto?.subtle;
|
|
22
27
|
function base64ToBytes(b64) {
|
|
@@ -27,6 +32,21 @@ function base64ToBytes(b64) {
|
|
|
27
32
|
}
|
|
28
33
|
return bytes;
|
|
29
34
|
}
|
|
35
|
+
function base64UrlToBytes(b64url) {
|
|
36
|
+
// Convert base64url to standard base64
|
|
37
|
+
let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
38
|
+
// Add padding
|
|
39
|
+
const pad = b64.length % 4;
|
|
40
|
+
if (pad === 2)
|
|
41
|
+
b64 += "==";
|
|
42
|
+
else if (pad === 3)
|
|
43
|
+
b64 += "=";
|
|
44
|
+
return base64ToBytes(b64);
|
|
45
|
+
}
|
|
46
|
+
function base64UrlDecode(b64url) {
|
|
47
|
+
const bytes = base64UrlToBytes(b64url);
|
|
48
|
+
return new TextDecoder().decode(bytes);
|
|
49
|
+
}
|
|
30
50
|
function bytesToHex(bytes) {
|
|
31
51
|
let hex = "";
|
|
32
52
|
for (let i = 0; i < bytes.length; i++) {
|
|
@@ -81,6 +101,63 @@ function parseResponse(response) {
|
|
|
81
101
|
const kid = data.kid;
|
|
82
102
|
return { data: { attestation, sig, kid } };
|
|
83
103
|
}
|
|
104
|
+
function parseJwt(token) {
|
|
105
|
+
const parts = token.split(".");
|
|
106
|
+
if (parts.length !== 3) {
|
|
107
|
+
throw new Error("Invalid JWT: expected 3 dot-separated segments");
|
|
108
|
+
}
|
|
109
|
+
const [headerB64, payloadB64, sigB64] = parts;
|
|
110
|
+
let header;
|
|
111
|
+
let payload;
|
|
112
|
+
try {
|
|
113
|
+
header = JSON.parse(base64UrlDecode(headerB64));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error("Invalid JWT: malformed header");
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
payload = JSON.parse(base64UrlDecode(payloadB64));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error("Invalid JWT: malformed payload");
|
|
123
|
+
}
|
|
124
|
+
const signatureBytes = base64UrlToBytes(sigB64);
|
|
125
|
+
return { header, payload, headerB64, payloadB64, signatureBytes };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Convert a DER-encoded ECDSA signature to raw IEEE P1363 format (r || s).
|
|
129
|
+
* jose/Node.js crypto may produce DER signatures; Web Crypto expects P1363.
|
|
130
|
+
*/
|
|
131
|
+
function derToP1363(der, keySize = 32) {
|
|
132
|
+
if (der[0] !== 0x30)
|
|
133
|
+
return der; // Not DER, assume already P1363
|
|
134
|
+
let offset = 2;
|
|
135
|
+
// Parse r
|
|
136
|
+
if (der[offset] !== 0x02)
|
|
137
|
+
return der;
|
|
138
|
+
offset++;
|
|
139
|
+
const rLen = der[offset];
|
|
140
|
+
offset++;
|
|
141
|
+
let r = der.slice(offset, offset + rLen);
|
|
142
|
+
offset += rLen;
|
|
143
|
+
// Parse s
|
|
144
|
+
if (der[offset] !== 0x02)
|
|
145
|
+
return der;
|
|
146
|
+
offset++;
|
|
147
|
+
const sLen = der[offset];
|
|
148
|
+
offset++;
|
|
149
|
+
let s = der.slice(offset, offset + sLen);
|
|
150
|
+
// Remove leading zero padding
|
|
151
|
+
if (r.length > keySize)
|
|
152
|
+
r = r.slice(r.length - keySize);
|
|
153
|
+
if (s.length > keySize)
|
|
154
|
+
s = s.slice(s.length - keySize);
|
|
155
|
+
// Pad to fixed keySize
|
|
156
|
+
const result = new Uint8Array(keySize * 2);
|
|
157
|
+
result.set(r, keySize - r.length);
|
|
158
|
+
result.set(s, keySize * 2 - s.length);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
84
161
|
// ── Verification checks ───────────────────────────────────────────
|
|
85
162
|
async function checkSignature(attestation, sig, keyJwk) {
|
|
86
163
|
if (!subtle) {
|
|
@@ -110,6 +187,35 @@ async function checkSignature(attestation, sig, keyJwk) {
|
|
|
110
187
|
};
|
|
111
188
|
}
|
|
112
189
|
}
|
|
190
|
+
async function checkJwtSignature(jwt, keyJwk) {
|
|
191
|
+
if (!subtle) {
|
|
192
|
+
return { passed: false, reason: "Web Crypto API not available" };
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
if (jwt.header.alg !== "ES256") {
|
|
196
|
+
return { passed: false, reason: `Unsupported JWT algorithm: ${jwt.header.alg}` };
|
|
197
|
+
}
|
|
198
|
+
const key = await subtle.importKey("jwk", keyJwk || PUBLIC_KEY_JWK, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
199
|
+
// JWT signature is over "header.payload" (the raw base64url segments)
|
|
200
|
+
const signingInput = new TextEncoder().encode(`${jwt.headerB64}.${jwt.payloadB64}`);
|
|
201
|
+
// JWT ES256 signatures should be raw r||s (P1363, 64 bytes)
|
|
202
|
+
// but some libraries produce DER encoding — handle both
|
|
203
|
+
let sigBytes = jwt.signatureBytes;
|
|
204
|
+
if (sigBytes.length !== 64) {
|
|
205
|
+
sigBytes = derToP1363(sigBytes);
|
|
206
|
+
}
|
|
207
|
+
const valid = await subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, sigBytes.buffer, signingInput);
|
|
208
|
+
return valid
|
|
209
|
+
? { passed: true }
|
|
210
|
+
: { passed: false, reason: "JWT signature does not match payload" };
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
return {
|
|
214
|
+
passed: false,
|
|
215
|
+
reason: `JWT signature verification error: ${e.message}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
113
219
|
async function checkConditionHashes(results) {
|
|
114
220
|
if (!subtle) {
|
|
115
221
|
return { passed: false, reason: "Web Crypto API not available" };
|
|
@@ -157,31 +263,92 @@ function checkFreshness(results, maxAge) {
|
|
|
157
263
|
}
|
|
158
264
|
return { passed: true };
|
|
159
265
|
}
|
|
160
|
-
function checkExpiry(
|
|
161
|
-
const
|
|
162
|
-
if (isNaN(
|
|
266
|
+
function checkExpiry(expiresAt) {
|
|
267
|
+
const ts = new Date(expiresAt).getTime();
|
|
268
|
+
if (isNaN(ts)) {
|
|
163
269
|
return { passed: false, reason: "Invalid expiresAt timestamp" };
|
|
164
270
|
}
|
|
165
|
-
if (Date.now() >
|
|
271
|
+
if (Date.now() > ts) {
|
|
166
272
|
return { passed: false, reason: "Attestation has expired" };
|
|
167
273
|
}
|
|
168
274
|
return { passed: true };
|
|
169
275
|
}
|
|
276
|
+
// ── JWT verification path ─────────────────────────────────────────
|
|
277
|
+
async function verifyJwt(token, options) {
|
|
278
|
+
let jwt;
|
|
279
|
+
try {
|
|
280
|
+
jwt = parseJwt(token);
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
return {
|
|
284
|
+
valid: false,
|
|
285
|
+
checks: {
|
|
286
|
+
signature: { passed: false, reason: `JWT parse error: ${e.message}` },
|
|
287
|
+
conditionHashes: { passed: false, reason: "Skipped (JWT parse failed)" },
|
|
288
|
+
freshness: { passed: false, reason: "Skipped (JWT parse failed)" },
|
|
289
|
+
expiry: { passed: false, reason: "Skipped (JWT parse failed)" },
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const p = jwt.payload;
|
|
294
|
+
// Resolve the signing key: use jwksUrl from options, or from JWT kid, or hardcoded
|
|
295
|
+
const jwksUrl = options?.jwksUrl || DEFAULT_JWKS_URL;
|
|
296
|
+
const kid = jwt.header.kid;
|
|
297
|
+
let keyJwk;
|
|
298
|
+
try {
|
|
299
|
+
keyJwk = await fetchJwksKey(jwksUrl, kid);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
return {
|
|
303
|
+
valid: false,
|
|
304
|
+
checks: {
|
|
305
|
+
signature: { passed: false, reason: `JWKS fetch error: ${e.message}` },
|
|
306
|
+
conditionHashes: { passed: false, reason: "Skipped (JWKS fetch failed)" },
|
|
307
|
+
freshness: { passed: false, reason: "Skipped (JWKS fetch failed)" },
|
|
308
|
+
expiry: { passed: false, reason: "Skipped (JWKS fetch failed)" },
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// Extract attestation data from JWT claims
|
|
313
|
+
const results = (p.results || []);
|
|
314
|
+
// JWT exp → expiresAt ISO string
|
|
315
|
+
const expUnix = p.exp;
|
|
316
|
+
const expiresAt = expUnix ? new Date(expUnix * 1000).toISOString() : "";
|
|
317
|
+
const [signature, conditionHashes, freshness, expiry] = await Promise.all([
|
|
318
|
+
checkJwtSignature(jwt, keyJwk),
|
|
319
|
+
checkConditionHashes(results),
|
|
320
|
+
Promise.resolve(checkFreshness(results, options?.maxAge)),
|
|
321
|
+
Promise.resolve(checkExpiry(expiresAt)),
|
|
322
|
+
]);
|
|
323
|
+
const valid = signature.passed &&
|
|
324
|
+
conditionHashes.passed &&
|
|
325
|
+
freshness.passed &&
|
|
326
|
+
expiry.passed;
|
|
327
|
+
return { valid, checks: { signature, conditionHashes, freshness, expiry } };
|
|
328
|
+
}
|
|
170
329
|
// ── Main export ────────────────────────────────────────────────────
|
|
171
330
|
/**
|
|
172
331
|
* Verify an InsumerAPI attestation response.
|
|
173
332
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
333
|
+
* Auto-detects input format:
|
|
334
|
+
* - **String** → JWT verification path (ES256 signature via JWKS)
|
|
335
|
+
* - **Object** → Raw attestation response path (ECDSA P1363 signature)
|
|
336
|
+
*
|
|
337
|
+
* Both formats run the same 4 checks:
|
|
338
|
+
* 1. **Signature** — ECDSA P-256 verification
|
|
176
339
|
* 2. **Condition hashes** — SHA-256 of canonical sorted-key JSON
|
|
177
340
|
* 3. **Freshness** — blockTimestamp age vs caller-defined maxAge (optional)
|
|
178
|
-
* 4. **Expiry** — whether the
|
|
341
|
+
* 4. **Expiry** — whether the attestation window has elapsed
|
|
179
342
|
*
|
|
180
|
-
* @param response
|
|
181
|
-
* @param options Optional configuration: maxAge (seconds)
|
|
343
|
+
* @param response JWT string or full API response object
|
|
344
|
+
* @param options Optional configuration: maxAge (seconds), jwksUrl
|
|
182
345
|
* @returns Structured result with overall validity and per-check details
|
|
183
346
|
*/
|
|
184
347
|
export async function verifyAttestation(response, options) {
|
|
348
|
+
// Auto-detect: string → JWT, object → raw attestation
|
|
349
|
+
if (typeof response === "string") {
|
|
350
|
+
return verifyJwt(response, options);
|
|
351
|
+
}
|
|
185
352
|
const parsed = parseResponse(response);
|
|
186
353
|
const { attestation, sig, kid } = parsed.data;
|
|
187
354
|
// If jwksUrl is provided, fetch the signing key dynamically
|
|
@@ -206,7 +373,7 @@ export async function verifyAttestation(response, options) {
|
|
|
206
373
|
checkSignature(attestation, sig, keyJwk),
|
|
207
374
|
checkConditionHashes(attestation.results),
|
|
208
375
|
Promise.resolve(checkFreshness(attestation.results, options?.maxAge)),
|
|
209
|
-
Promise.resolve(checkExpiry(attestation)),
|
|
376
|
+
Promise.resolve(checkExpiry(attestation.expiresAt)),
|
|
210
377
|
]);
|
|
211
378
|
const valid = signature.passed &&
|
|
212
379
|
conditionHashes.passed &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insumer-verify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Client-side verifier for InsumerAPI attestations. ECDSA P-256 signatures, condition hashes, block freshness, expiry. Zero dependencies. Used by DJD Agent Score (Coinbase x402).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|