This page checks the ES256 signature in your browser, against our public key. JWT or SD-JWT VC. Nothing you paste is sent to us.
Verification is public and free. You need an account to create credentials — never to check one.
Runs entirely in your browser with WebCrypto. After the key set loads, we make no further requests — open your network tab and watch.
No credential handy? Tamper flips one character of the payload and re-verifies. One changed byte breaks the signature.
Key set: not loaded yet.
—
—
ID and liveness passed. You never see their name or face.
The seal names what was done. Change it and the check fails.
The time is signed with everything else.
If we went dark tomorrow, every credential we issued would still verify. That’s the only way the proof means anything.
You fetch the public key set once over HTTPS and cache it. After that, every check is a local signature check.
It isn’t magic. The first key fetch needs the internet, and you should refresh the key set to pick up rotations.
A credential also carries an expiry. Checking it is your job — one line.
Most vendors return a score, worth only your trust in the vendor. You can’t audit it, reproduce it, or check it after they’re gone.
We return a signature. It is valid or it isn’t, and anyone with the public key can settle that — without asking us.
GET https://api.5arz.com/.well-known/jwks.json — public, CORS-open, no auth.
In memory, on disk, or in your repo. Refresh daily to pick up rotation. Keys are identified by kid, so old and new coexist — with one exception, below.
kidDecode the JWT header and find the key with that kid. The live signing key today is 5arz-oracle-2.
Standard P-256 / SHA-256, in every language. Then check exp and read what the credential asserts.
Our policy: a retired public key stays in the key set permanently. Rotating a signing key must never stop an already-issued credential from verifying.
There is one exception, and we’d rather you read it here. An earlier key, 5arz-oracle-1, was retired before that policy existed, and its public half was not kept.
kid: 5arz-oracle-1 cannot be verified — not by you, and not by us. We don’t present that vintage as verifiable. It covers roughly a dozen test credentials from our partner path; none is in production use.Everything signed since — every credential under 5arz-oracle-2 — is covered by the retention policy, and the key set is built to serve retired keys alongside the live one.
Complete and runnable. No 5arz SDK needed — any JWT library can verify a seal. For an SD-JWT VC, verify the part before the first ~.
// npm i jose import { createRemoteJWKSet, jwtVerify } from 'jose'; // Fetched once, then cached in-process and refreshed on rotation. const JWKS = createRemoteJWKSet( new URL('https://api.5arz.com/.well-known/jwks.json'), { cacheMaxAge: 86400000 } // 24h ); export async function verifySeal(token) { const { payload, protectedHeader } = await jwtVerify(token, JWKS, { algorithms: ['ES256'], issuer: 'https://5arz.com' }); // Signature is valid and exp/nbf were enforced by jwtVerify. return { ok: true, kid: protectedHeader.kid, claims: payload }; }
# pip install pyjwt[crypto] requests import jwt, requests, time from jwt import PyJWKClient # PyJWKClient caches the key set and only refetches on an unknown kid. _jwks = PyJWKClient("https://api.5arz.com/.well-known/jwks.json", cache_keys=True, lifespan=86400) def verify_seal(token: str) -> dict: key = _jwks.get_signing_key_from_jwt(token).key claims = jwt.decode( token, key, algorithms=["ES256"], issuer="https://5arz.com", ) # raises on bad signature or expiry return claims
// go get github.com/lestrrat-go/jwx/v2 package seal import ( "context" "time" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" ) const jwksURL = "https://api.5arz.com/.well-known/jwks.json" var cache = jwk.NewCache(context.Background()) func Init() { cache.Register(jwksURL, jwk.WithMinRefreshInterval(24*time.Hour)) } func VerifySeal(ctx context.Context, token []byte) (jwt.Token, error) { set, err := cache.Get(ctx, jwksURL) // served from cache if err != nil { return nil, err } return jwt.Parse(token, jwt.WithKeySet(set), jwt.WithValidate(true), jwt.WithIssuer("https://5arz.com"), ) }
# Look at the key set — this is the only network call verification needs. curl -s https://api.5arz.com/.well-known/jwks.json # { # "keys": [{ # "kty": "EC", "crv": "P-256", # "x": "amC_s6qLClaKsDqE5594am8QSc5GP6agTTbjbM_jRdM", # "y": "GYq97U3A7ZuBGJdGEnC_f6dq6HG4MS1RBuKRhJf1T2U", # "kid": "5arz-oracle-2", "alg": "ES256", # "use": "sig", "key_ops": ["verify"] # }] # } # Save it. From here on, verification is local: curl -s https://api.5arz.com/.well-known/jwks.json > jwks.json # Read a credential's claims without verifying (inspection only — # NEVER trust these until the signature checks out): echo "$JWT" | cut -d. -f2 | base64 -d 2>/dev/null | jq .
// No dependencies — WebCrypto is in every modern browser and in // Node 16+ via globalThis.crypto. This is what the widget above runs. const b64u = s => Uint8Array.from( atob(s.replace(/-/g,'+').replace(/_/g,'/') .padEnd(Math.ceil(s.length/4)*4,'=')), c => c.charCodeAt(0)); async function verify(token, jwks) { // An SD-JWT VC is "<jws>~…" — the signature covers the part before the first ~ const [h, p, s] = token.split('~')[0].split('.'); const hdr = JSON.parse(new TextDecoder().decode(b64u(h))); const jwk = jwks.keys.find(k => k.kid === hdr.kid); if (!jwk) throw new Error('unknown kid: ' + hdr.kid); // IMPORTANT: strip use/key_ops before importKey. Chrome and Safari // reject a JWK whose key_ops does not exactly match the usages you // request, and our JWKS publishes both fields. This one line is the // difference between working and a DataError. const { use, key_ops, ...clean } = jwk; const key = await crypto.subtle.importKey( 'jwk', clean, { name:'ECDSA', namedCurve:'P-256' }, false, ['verify']); return crypto.subtle.verify( { name:'ECDSA', hash:'SHA-256' }, key, b64u(s), new TextEncoder().encode(h + '.' + p)); }
In the Browser tab: the use / key_ops strip is what trips people up. WebCrypto is stricter than the JWKS spec; other libraries handle it for you.
A valid signature means authentic and unmodified. What the credential says is in the claims — read them.
| Claim | What it asserts |
|---|---|
iss | Who signed it. Should be https://5arz.com — pin this, don’t just check the signature. |
iat / exp | When it was issued and when it stops being valid. Enforce exp. |
sub | The subject the proof is about — a session, a task, or a piece of content. |
test | Present and true on sandbox credentials. A test credential is not proof of anything. Reject it in production. |
You can — we have one. Here’s what changes when verification is local.
| Question | Vendor API check | Offline signature check |
|---|---|---|
| Works if the vendor is down | No | Yes |
| Works if the vendor is gone | No | Yes |
| Vendor sees what you verify | Yes | No |
| Latency | Network round trip | Sub-millisecond, local |
| Rate limited | Usually | No |
| Costs money at scale | Usually | Free, unlimited |
| Auditable by a third party | No | Yes |
The honest caveat: an offline check can’t know about a revocation issued after your key set was cached. That’s true of every signed credential — it’s why certificates have revocation lists.
We publish a W3C Bitstring Status List — one bit per credential in a single cacheable file. You download the whole list, so we never learn which credential you checked.
A revoked credential stops verifying within 15 minutes if you follow our refresh interval, and immediately if you check status live. Lost-device revocation is immediate everywhere: it deletes the key.
We state the window rather than claim zero. No signature-based credential can have a zero window.
No. A 5arz credential is an ES256 JWT signed by a P-256 key. You fetch the public key set once from https://api.5arz.com/.well-known/jwks.json, cache it, and every verification after that is a local signature check on your own hardware. No account, no API key, no network call at verify time.
Authenticity and expiry keep verifying against public keys you already hold, with no dependency on 5arz being reachable. Revocation status is the one check that needs a published status list; our stated refresh window is 15 minutes.
An ES256 signature check on a cached key is sub-millisecond on ordinary server hardware. There is no network round trip because there is no network call.
No. Verification is public and free with no account. You only need an account to create credentials.
Start free. Checking stays free, always.