@certnode/verify
v3.3.0
Published
Verify-only SDK for CertNode AI Provenance receipts. No API key required — verification is public. Lightweight alternative to @certnode/sdk for browser extensions, audit tooling, and verification pipelines. Complete rewrite from v1.x (unrelated to previou
Maintainers
Readme
@certnode/verify
Verify-only SDK for CertNode AI Provenance receipts.
Verification is public — no API key required. This package is the lightweight alternative to @certnode/sdk for callers that only need to verify, not sign.
When to use this package
- Browser extensions verifying signatures on web pages (Chrome/Firefox/Safari/Edge)
- Audit tooling pulling receipts from logs without holding signing keys
- Verification pipelines on third-party content
- Server-side verify-before-serve gates where you can't ship a signing key
For signing, use @certnode/sdk which includes the full signing + verifying + searching surface.
Install
npm install @certnode/verifyQuickstart
import { CertNodeVerify } from '@certnode/verify'
const verifier = new CertNodeVerify()
// Mode 1: by receipt ID
const result = await verifier.verify({ receiptId: 'uuid-here' })
console.log(result.valid) // true / false
console.log(result.receipt?.signedAt) // ISO timestamp
console.log(result.receipt?.timestamps.bitcoin?.status) // 'anchored' / 'pending'
// Mode 2: by raw signature + content (no DB lookup)
const recheck = await verifier.verify({
signature: '...',
content: 'original content',
})
console.log(recheck.signatureValid)
console.log(recheck.contentMatches)Local / offline verification (no network)
The methods above (verify, getReceipt, getTrustScore) call CertNode's
verify endpoint and return the server's verdict. verifyOffline instead runs
the cryptography in your own process — it never asks CertNode's server to
grade its own work. This is what makes a receipt independently verifiable:
opposing counsel, an auditor, or a regulator can reproduce the verdict from the
receipt plus CertNode's published public keys, with CertNode offline.
import { CertNodeVerify, verifyOffline, fetchJwks } from '@certnode/verify'
// 1) Fully offline — supply the JWKS yourself (e.g. pinned/bundled from
// https://certnode.io/.well-known/jwks.json). ZERO network calls.
const jwks = { keys: [ /* ...published EC P-256 keys... */ ] }
const result = await verifyOffline(receipt, originalContent, { jwks })
console.log(result.valid) // signature valid AND content matches
console.log(result.signatureValid) // ES256 JWS intact under the published key
console.log(result.contentMatches) // re-hash(content) === signed contentHash
// 2) Or fetch + cache the published JWKS once, then verify locally:
const result2 = await verifyOffline(receipt, originalContent)
// Also available as a method on the client:
const verifier = new CertNodeVerify()
const result3 = await verifier.verifyOffline(receipt, originalContent, { jwks })receipt accepts the receipt object ({ signature } or { jws }) or a bare
compact-JWS string. content is optional: omit it to check only that the
signature is cryptographically intact; pass it to also confirm the content
hasn't changed since signing.
What verifyOffline checks, locally:
- ES256 JWS signature — decodes the compact JWS, selects the EC P-256
public key from the JWKS by the JWS
kid, and verifies the signature withjose. The algorithm is pinned toES256(analg: none/HS256downgrade is rejected before any key import). An unknownkidis a hard failure — it never falls back to an arbitrary key. - Content hash — when you pass
content, it is SHA-256 hashed (UTF-8), byte-for-byte the way the signer hashes it, and compared to the signedcontentHashclaim. - Issuer (optional) — pass
options.issuer(or includeissueron the JWKS) to additionally require the JWSissclaim to match. - Subject / anti-swap (optional) — pass
options.expectedSubject(the receipt id you expect) to require the signedsubclaim to equal it, so a valid signature lifted from another receipt cannot be presented as this one. Supply it whenever you know which receipt you are verifying; without it the offline check verifies signature + content but cannot detect a receipt-id swap.
interface OfflineVerifyOptions {
jwks?: Jwks // supply for fully-offline verification (no network)
jwksUrl?: string // else fetch once from here (default: published .well-known)
issuer?: string // optionally enforce the JWS issuer claim
expectedSubject?: string // ANTI-SWAP: enforce the signed `sub` equals this receipt id
timeoutMs?: number // JWKS fetch timeout (default 15000)
}
interface OfflineVerifyResult {
valid: boolean // signatureValid AND (content ? contentMatches : true) AND (expectedSubject ? subjectMatches : true)
signatureValid: boolean
contentMatches: boolean | null // null when no content was provided
subjectMatches?: boolean | null // null when no expectedSubject was provided (subject unchecked)
payload?: Record<string, unknown>
kid?: string
error?: string // e.g. invalid_signature, content_mismatch, subject_mismatch, unknown_kid:<kid>
}Failure modes return { valid: false, error } rather than throwing, except a
missing/unobtainable JWKS (a misconfiguration) which throws.
Where to get the public keys: CertNode publishes its EC P-256 signing keys
at https://certnode.io/.well-known/jwks.json (the default jwksUrl). For a
truly air-gapped check, copy that key set into your code/config and pass it as
options.jwks.
Scope note — RFC 3161 / Bitcoin layers:
verifyOfflineverifies the JWS signature and the content hash locally. It does not yet verify the RFC 3161 timestamp token or the Bitcoin/OpenTimestamps anchor offline — those remain verified via the networkverify({ receiptId })path (which returns a chain-trusted RFC 3161 verdict). Local timestamp-token verification is a planned follow-on. Don't readverifyOfflinevalid:trueas "the timestamp chain was checked" — it means "this content carries an intact CertNode ES256 signature over its hash, verified against CertNode's published key."
API
new CertNodeVerify(options?)
interface CertNodeVerifyOptions {
baseUrl?: string // default: https://certnode.io/api/v1/provenance
timeoutMs?: number // default: 15000
}No API key needed — verification is public.
verifier.verify(input)
interface VerifyInput {
// Mode 1: by receipt ID
receiptId?: string
// Mode 2: by raw signature + content
signature?: string
content?: string
}Returns VerifyResult with valid, signatureValid, contentMatches, and the full receipt object when mode 1.
verifier.getReceipt(receiptId)
Convenience wrapper around verify({ receiptId }). Returns just the receipt object if valid; throws CertNodeVerifyError with code 'not_found' otherwise.
const receipt = await verifier.getReceipt('uuid-here')
console.log(receipt.signedAt)
console.log(receipt.model, receipt.provider)
console.log(receipt.timestamps.certnode.id)
console.log(receipt.timestamps.rfc3161) // base64 DER token (optional)
console.log(receipt.timestamps.bitcoin?.status) // 'anchored' / 'pending' / 'skipped'verifier.getTrustScore(receiptId)
Compute a developer-facing trust score (0-100) from a receipt's verification signals.
const trust = await verifier.getTrustScore('uuid-here')
console.log(trust.score) // 70
console.log(trust.signals.bitcoinAnchored) // true
console.log(trust.signals.ageHours) // 26.4Weighting: signature 40 + RFC 3161 anchored 20 + Bitcoin anchored 30 + age bonuses (5 at 1h, 5 at 24h).
Not a regulatory or legal metric — a developer-facing convenience number. For legal use, cite the underlying receipt's three-layer chain directly; self-authentication under FRE 902(13)/(14) is a property of the record, while admissibility is a court's decision.
What gets verified
When you call verify({ receiptId }), CertNode confirms:
- JWS signature is cryptographically intact — ES256 over the receipt payload, using CertNode's published EC P-256 public key.
- Content hash matches — if you pass
content, CertNode re-hashes it and compares to the signed hash. Mismatch means the content has been modified since signing. - Three-layer timestamp chain — receipt includes (a) CertNode signing timestamp, (b) RFC 3161 token from an independent Time Stamp Authority, (c) Bitcoin OpenTimestamps anchor status.
A fully-verified receipt is designed to support self-authentication under FRE 902(13)/(14) for digital evidence (self-authentication is a property of the record; admissibility remains a court's decision). The verification is reproducible — opposing counsel / regulators / auditors can run the same check independently.
Errors
import { CertNodeVerifyError } from '@certnode/verify'
try {
await verifier.verify({ receiptId })
} catch (err) {
if (err instanceof CertNodeVerifyError) {
console.log(err.code) // e.g. 'not_found'
console.log(err.status) // HTTP status
}
}Common error codes:
invalid_request— neither receiptId nor signature+content passedreceipt_not_found— no receipt with this IDreceiptId_required— empty receiptId togetReceipt()timeout— request exceeded timeoutMsnetwork_error— fetch failed
Migrating to v3.0.0
v3.0.0 tightens verification soundness (tri-agent security review, 2026-06-06). It is backward-compatible for every CertNode receipt in production — all use SHA-256/384/512 and verify unchanged. Changes:
- BREAKING (rare): RFC 3161 timestamp tokens whose message-imprint uses an unrecognized or
weak hash-algorithm OID (e.g. SHA-1) now fail verification (
messageImprintMatchesmust be exactlytrue) rather than passing on signature + chain alone. No production CertNode token is affected — this only rejects non-standard / weak-hash tokens that should never have passed. - Stricter offline
algcheck: a JWS whose protected header is missingalg(or is notES256) is now rejected up front, matching the server. Genuine receipts always carryalg: ES256, so they are unaffected. - New
expectedSubjectoption (anti-swap):verifyOffline(receipt, content, { expectedSubject })enforces that the signedsubequals the receipt id you expect. Without it, the offline verifier checks signature + content but cannot detect a receipt-id swap — passexpectedSubjectwhenever you know which receipt you are verifying. The result gains asubjectMatchesfield.
To upgrade: if you call verifyOffline and know the receipt id, add expectedSubject. If you
verify third-party RFC 3161 tokens with non-SHA-2 hashes (CertNode never emits these), test before
upgrading. Otherwise no code changes are required.
Links
- Homepage: https://certnode.io/ai-provenance
- Docs: https://certnode.io/docs/provenance
- Full SDK (sign + verify + search): https://www.npmjs.com/package/@certnode/sdk
- Public verify page: https://certnode.io/verify/[receiptId]
- Source: https://github.com/srbryant86/certnode/tree/main/packages/verify
- License: MIT
