← InsumerAPICONTENT HISTORY
WHAT CHANGED
Update to InsumerAPI
Snapshot Oct 28, 2026 · 18:06 UTC · version 0.2.1
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save InsumerAPI and check future changes in Following.
Up to 10 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Offline ES256 verification of InsumerAPI signed responses against the public JWKS. Use when the user receives a signed attestation from /v1/attest, /v1/trust, or /v1/trust/batch and needs to verify the signature without trusting the JSON body. Covers both the JWT path (jose / PyJWT / go-jose) and the raw sig path (ES256 over the canonical attestation/trust object).\n",
"included_files": [
{
"relative_path": "references/jwks-format.md",
"size_in_bytes": 10492
},
{
"relative_path": "scripts/verify.py",
"size_in_bytes": 3369
}
],
"name": "insumer-jwks-verify",
"skill_md_contents": "---\nname: insumer-jwks-verify\ndescription: >\n Offline ES256 verification of InsumerAPI signed responses against the public\n JWKS. Use when the user receives a signed attestation from /v1/attest,\n /v1/trust, or /v1/trust/batch and needs to verify the signature without\n trusting the JSON body. Covers both the JWT path (jose / PyJWT / go-jose) and\n the raw sig path (ES256 over the canonical attestation/trust object).\nmetadata:\n version: \"0.2.1\"\n author: InsumerAPI\n---\n\n# InsumerAPI Offline JWKS Verification\n\nThe signed boolean is the product. The JSON body alone is untrusted: anyone can fabricate a JSON response. **Verify the signature, every time.**\n\nInsumerAPI signs every `/v1/attest`, `/v1/trust`, and `/v1/trust/batch` response twice: with ES256 (ECDSA P-256) and with a post-quantum ML-DSA-65 signature. The public keys are published as a standard JWKS at `https://insumermodel.com/.well-known/jwks.json`. The signing key never leaves the issuer; anyone holding the JWKS can independently re-run the verification, with no callback to InsumerAPI.\n\n## Reference values (do not hallucinate)\n\n- **JWKS URL**: `https://insumermodel.com/.well-known/jwks.json`\n- **Algorithm**: ES256 (ECDSA P-256)\n- **Key IDs (`kid`)**: five JWKS entries over two keys. Three EC kids on the same P-256 key: `insumer-attest-v2` (attest, every key issued since 2026-06-10), `insumer-trust-v2` (trust), `insumer-attest-v1` (pre-cutover keys, and the commerce discount path). Then two RFC 9964 `AKP` entries for the ML-DSA-65 post-quantum key: `insumer-attest-pq1` and `insumer-trust-pq1`, selected by the response `pqKid`. Resolve by kid, never by position; fail closed on an unknown kid.\n- **JWT issuer claim** (when `format: \"jwt\"` is requested): `https://api.insumermodel.com`\n- **Raw signature format**: base64 P1363 (88 chars) on the `sig` field\n\n## Two verification paths\n\nInsumerAPI returns up to **two** verifiable forms in a signed response:\n\n1. **`sig` field** (every response): base64 P1363 ES256 signature over the preimage the `kid` selects (see \"What's signed\" in `references/jwks-format.md`). Attestations: for `insumer-attest-v2`, the domain tag `insumer.attestation.v2` + newline + canonical JSON (keys sorted recursively, no whitespace) of `{v:2, id, pass, results, attestedAt}`; for `insumer-attest-v1`, `JSON.stringify({id, pass, results, attestedAt})` in that insertion order. Trust profiles: for `insumer-trust-v2`, the domain tag `insumer.trust.v2` + newline + canonical JSON of the whole `trust` object; for `insumer-attest-v1`, `JSON.stringify(trust)` as issued. Verify with any ES256 library + the JWKS key the `kid` names. Responses also carry `pqSig`/`pqKid`, a post-quantum ML-DSA-65 signature over the same preimage under a post-quantum domain tag (spec Check 6). The two fields are optional: a stored artifact without them reads as `absent`, which is not a failed signature.\n2. **`jwt` field** *(`/v1/attest` only, and only when `\"format\": \"jwt\"` is in the request body; `/v1/trust` and `/v1/trust/batch` have no JWT form)*: standard ES256 JWT carrying the attestation as claims, beside `pqJwt`, the same claims under the post-quantum signature. Verify with any standard JWT library pointed at the JWKS URL.\n\nThe `jwt` path is easier when the consumer is already using a JWT library, and it signs the wallet as the `sub` claim. The raw `sig` path is more compact, but for most condition types it does not sign the wallet: it proves that some wallet met the condition, not which one. Use the JWT form (or an `erc8004_agent` / `erc7710_delegation` condition, which carry the wallet in the signed result) whenever the relying party must know which wallet was attested.\n\n## Recipe 1: JWT verification (Node.js, `jose`)\n\nAdd `\"format\": \"jwt\"` to the `/v1/attest` request body (trust profiles have no JWT form; use Recipe 3), then:\n\n```javascript\nimport { createRemoteJWKSet, jwtVerify } from 'jose';\n\nconst JWKS = createRemoteJWKSet(\n new URL('https://insumermodel.com/.well-known/jwks.json')\n);\n\nasync function verifyAttestation(jwtString) {\n const { payload } = await jwtVerify(jwtString, JWKS, {\n issuer: 'https://api.insumermodel.com',\n algorithms: ['ES256'],\n });\n // payload.pass is the verified boolean\n // payload.conditionHash, payload.blockNumber, payload.blockTimestamp\n // are also verified as part of the signed JWT\n return payload;\n}\n```\n\n`createRemoteJWKSet` caches the JWKS automatically with sane defaults. Don't fetch the JWKS yourself on every call.\n\n## Recipe 2: JWT verification (Python, `PyJWT` + `cryptography`)\n\n```python\nimport jwt\nfrom jwt.jwks_client import PyJWKClient\n\njwks_client = PyJWKClient(\"https://insumermodel.com/.well-known/jwks.json\")\n\ndef verify_attestation(jwt_string: str) -> dict:\n signing_key = jwks_client.get_signing_key_from_jwt(jwt_string)\n payload = jwt.decode(\n jwt_string,\n signing_key.key,\n algorithms=[\"ES256\"],\n issuer=\"https://api.insumermodel.com\",\n )\n # payload[\"pass\"] is the verified boolean\n return payload\n```\n\n## Recipe 3: Raw `sig` verification (Node.js)\n\nFor any response's `sig` (every trust profile, and attestations without `format: \"jwt\"`). Pass `response.data` for `/v1/attest` and `/v1/trust`, or one entry of `data.results[]` for `/v1/trust/batch`, exactly as parsed from the wire; never rebuild or re-order the signed object, because the v1 scheme signs insertion-order `JSON.stringify` output.\n\n```javascript\n// Canonical JSON: keys sorted recursively at every level, no whitespace.\nfunction canonicalize(value) {\n if (Array.isArray(value)) return '[' + value.map(canonicalize).join(',') + ']';\n if (value && typeof value === 'object') {\n return '{' + Object.keys(value).sort()\n .map((k) => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}';\n }\n return JSON.stringify(value);\n}\n\n// The exact bytes the ES256 signature covers, selected by kid.\nfunction signedPreimage(data) {\n const { kid } = data;\n if (data.attestation) {\n const { id, pass, results, attestedAt } = data.attestation;\n if (kid === 'insumer-attest-v2') {\n return 'insumer.attestation.v2\\n' + canonicalize({ v: 2, id, pass, results, attestedAt });\n }\n if (kid === 'insumer-attest-v1') return JSON.stringify({ id, pass, results, attestedAt });\n } else if (data.trust) {\n if (kid === 'insumer-trust-v2') return 'insumer.trust.v2\\n' + canonicalize(data.trust);\n if (kid === 'insumer-attest-v1') return JSON.stringify(data.trust);\n }\n throw new Error(`kid ${kid} does not sign this artifact`); // fail closed\n}\n\nasync function verifyRawSig(data, jwks) {\n const jwk = jwks.keys.find((k) => k.kty === 'EC' && k.kid === data.kid);\n if (!jwk) throw new Error(`unknown kid ${data.kid}`); // fail closed\n const key = await crypto.subtle.importKey(\n 'jwk', { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y },\n { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);\n // Web Crypto takes the 64-byte P1363 (r || s) signature as is.\n return crypto.subtle.verify(\n { name: 'ECDSA', hash: 'SHA-256' }, key,\n Buffer.from(data.sig, 'base64'),\n new TextEncoder().encode(signedPreimage(data)));\n}\n```\n\nThis checks the ES256 signature only. It does not check condition hashes, expiry, or the post-quantum signature. For all of those, use the official package, **`insumer-verify`** on npm (ES module; import it, do not `require` it):\n\n```bash\nnpm install insumer-verify @noble/post-quantum\n```\n\n`@noble/post-quantum` is an optional peer of `insumer-verify`. Without it, every response still verifies classically, but `checks.pq.status` reports `unverifiable` for a post-quantum signature that is present, so the post-quantum signature is never actually checked.\n\nIn Python, the same package is on PyPI under the same name, runs the same checks and passes the same 27 published test vectors:\n\n```bash\npip install \"insumer-verify[pq]\"\n```\n\n```python\nfrom insumer_verify import verify_attestation, verify_trust_profile\n\nopts = {\"jwks_url\": \"https://insumermodel.com/.well-known/jwks.json\"}\nresult = verify_attestation(response, **opts) # the full /v1/attest response, or a JWT string\nif result[\"valid\"] and response[\"data\"][\"attestation\"][\"pass\"]:\n ...\nprofile = verify_trust_profile(trust_response, **opts) # render profile[\"trust\"] only when profile[\"valid\"]\n```\n\nWithout `dilithium-py` (the `[pq]` extra) the post-quantum signature is reported `unverifiable`, exactly as the npm package behaves without `@noble/post-quantum`.\n\n```javascript\nimport { verifyAttestation, verifyTrustProfile } from 'insumer-verify';\n\nconst opts = { jwksUrl: 'https://insumermodel.com/.well-known/jwks.json' };\n\n// /v1/attest: pass the full response ({ ok, data: { attestation, sig, kid, pqSig, pqKid, ... } }).\nconst result = await verifyAttestation(response, opts);\n// result.valid is the AND of the checks; result.checks reports each one separately:\n// checks.signature, checks.conditionHashes, checks.freshness, checks.expiry, and\n// checks.pq (the post-quantum signature: status verified | refuted | absent | unverifiable).\n// For a whole format:\"jwt\" response (data.jwt beside data.attestation) there is also\n// checks.jwt, which verifies the tokens and binds them to the attestation.\nif (!result.valid) throw new Error('attestation rejected: ' + JSON.stringify(result.checks));\n\n// /v1/trust: pass the full response; for /v1/trust/batch, call once per data.results[i].\nconst trust = await verifyTrustProfile(trustResponse, opts);\n// trust.checks: signature, freshness, expiry, pq. Render trust.trust (the verified object), gated on trust.valid.\nif (!trust.valid) throw new Error('trust profile rejected: ' + JSON.stringify(trust.checks));\n```\n\n## Recipe 4: Conditional verification + tamper detection\n\nBeyond signature verification, you can independently re-derive the `conditionHash` to confirm the condition wasn't tampered with:\n\n```javascript\nimport { createHash } from 'node:crypto';\n\n// conditionHash = \"0x\" + SHA-256 over evaluatedCondition, serialized per the kid:\n// v2 kids: canonical JSON, keys sorted recursively at every level, no whitespace.\n// insumer-attest-v1: JSON.stringify with the sorted top-level keys as the replacer.\nfunction canonicalize(value) {\n if (Array.isArray(value)) return '[' + value.map(canonicalize).join(',') + ']';\n if (value && typeof value === 'object') {\n return '{' + Object.keys(value).sort()\n .map((k) => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}';\n }\n return JSON.stringify(value);\n}\n\nfunction recomputeConditionHash(evaluatedCondition, kid) {\n const bytes = kid === 'insumer-attest-v1'\n ? JSON.stringify(evaluatedCondition, Object.keys(evaluatedCondition).sort())\n : canonicalize(evaluatedCondition);\n return '0x' + createHash('sha256').update(bytes).digest('hex');\n}\n\n// After signature verification (Recipe 1), re-derive and compare every condition.\n// In the JWT payload, conditionHash is an array (one entry per condition) and each\n// evaluatedCondition lives in results[i]. kid is the JWT header's kid\n// (jwtVerify returns it as protectedHeader.kid).\npayload.results.forEach((r, i) => {\n const recomputed = recomputeConditionHash(r.evaluatedCondition, kid);\n if (recomputed !== r.conditionHash || recomputed !== payload.conditionHash[i]) {\n throw new Error(`conditionHash mismatch on condition ${i}: payload may have been tampered with`);\n }\n});\n```\n\nFor the raw form, the same loop runs over `data.attestation.results` with `data.kid`.\n\n## Code emission rules\n\n1. **Cache the JWKS, not the verdict.** Libraries like `jose`'s `createRemoteJWKSet` and `PyJWT`'s `PyJWKClient` cache automatically with TTL. Do not cache `pass`: wallet state changes and the attestation has an `expiresAt` 30 minutes out (5 with a delegation condition).\n2. **Pin the algorithm.** Always pass `algorithms: ['ES256']`. Never accept any algorithm. This blocks \"alg confusion\" attacks.\n3. **Pin the issuer.** Always pass `issuer: 'https://api.insumermodel.com'` for JWT verification.\n4. **Verify in the trust boundary.** Verify on the server that's making the access decision. Never verify in the browser and trust the result. (Browsers can verify; they just can't be the trust boundary.)\n5. **Fail closed.** If verification throws, deny access. Never default to \"allow\" on verification failure.\n\n## Helper script\n\n`scripts/verify.py`: Python helper that takes a JWT, or a response object carrying `jwt` (from `/v1/attest` with `format: \"jwt\"`), on stdin and verifies the JWT against the public JWKS. Prints `OK` + payload, or `INVALID` + reason. Given a response with no `jwt`, or when the signing key cannot be fetched or resolved, it prints `NOT CHECKED` and exits non-zero: that is not a verdict on the token. It does not verify raw `sig` responses (every trust profile, and attestations without `format: \"jwt\"`), condition hashes, the expiry binding or the post-quantum signature. For every check in Python, `pip install \"insumer-verify[pq]\"` (above) is the complete path (without the `[pq]` extra the post-quantum signature is reported `unverifiable`); use Recipe 3 or `insumer-verify` on npm in JavaScript.\n\n```bash\necho '{\"jwt\":\"eyJhbG...\",\"kid\":\"insumer-attest-v2\"}' | python scripts/verify.py\n```\n\n## Error handling\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| \"unknown kid\" | A stale JWKS cache, or a result InsumerAPI did not sign | Refresh the JWKS. Kids are never removed, so one that still does not resolve is unverifiable: do not accept the result |\n| \"JWT signature invalid\" | Payload tampered, or wrong public key | Confirm `kid` matches a JWKS entry, confirm algorithm pinned to ES256 |\n| \"JWT issuer mismatch\" | Issuer claim doesn't match `https://api.insumermodel.com` | Confirm response actually came from InsumerAPI |\n| \"JWT expired\" | Past its `exp` (30 min, or 5 with a delegation condition) | Re-request a fresh attestation; do not extend TTL |\n| \"conditionHash mismatch\" | Condition object was modified after signing | Untrusted payload: reject |\n\n## Related skills\n\n| Skill | Purpose |\n|---|---|\n| `insumer-auth` | Get a key (verifying responses doesn't need a key, but requesting them does) |\n| `insumer-attest` | Produces signed responses to verify |\n| `insumer-trust` | Produces signed responses to verify |\n| `insumer-trust-batch` | Verify each profile entry independently |\n\n## References\n\n- [jwks-format.md](references/jwks-format.md): full JWKS document shape, key rotation policy, JWT claim list\n- [`insumer-verify` on npm](https://www.npmjs.com/package/insumer-verify): canonical raw-sig verification package\n- [Public JWKS](https://insumermodel.com/.well-known/jwks.json): fetch the live key set\n- [JWT spec (RFC 7519)](https://datatracker.ietf.org/doc/html/rfc7519)\n- [JWS spec (RFC 7515)](https://datatracker.ietf.org/doc/html/rfc7515)\n"
}SHA-256 of public snapshot: 7f81cdf8d1380efc4a019ee44c176f4fa518c29daed83dc76d977a324aab6d5f