Trust Center attestation
Every published Trust Center page that is current (evaluated in the last 24 hours) has a signed, machine-readable copy: a compact JSON Web Token (JWT) that carries the exact public payload the page renders. Insurer, auditor and procurement tooling can fetch it, verify the signature against the deployment’s public keys, and read the same controls a person sees on the page.
While the page is current, it links to the copy in its footer (“Machine-readable, signed copy”) and in its HTML head as <link rel="alternate" type="application/jwt">. A stale page shows no link, and its attestation URL answers 503 (see below).
What it proves, and what it does not
Section titled “What it proves, and what it does not”An attestation proves that this Breeze installation computed this payload, for this page, at this time. The signature binds the payload to the page URL and to the time of the last evaluation, so the token cannot be edited or replayed against another page without detection.
It does not prove that the organization is secure, compliant or certified. The payload is an evidence summary from Breeze telemetry, with the same limits as the page itself.
Endpoints
Section titled “Endpoints”| URL | Returns |
|---|---|
GET /api/v1/trust/attestation/<partner>/<org> |
The attestation for an organization page, as application/jwt |
GET /api/v1/trust/attestation/<partner> |
The attestation for a partner home page |
GET /api/v1/trust/attestation/jwks.json |
The public verification keys, as application/jwk-set+json |
All three are public, need no authentication and allow cross-origin reads (Access-Control-Allow-Origin: *, never with credentials). They are rate limited to 60 requests per minute per IP address.
| Response | When |
|---|---|
200 |
The page is published and was evaluated in the last 24 hours. Cache-Control: public, max-age=60 or less (never past the token’s exp). |
404 {"error":"Not found","code":"NOT_FOUND"} |
The page does not exist, is not published, or cannot be served. The response is the same for every cause. |
503 {"code":"ATTESTATION_STALE"} with Retry-After: 3600 |
The page has not been evaluated in the last 24 hours. No token is issued for a stale page. |
503 {"code":"SERVICE_UNAVAILABLE"} |
Attestation URLs: the installation has no active signing key, or no public URL configured (see Self-hosting). JWKS: every key has been revoked, so there is nothing to publish. While there is no active key, the JWKS still answers 200 with the verify-only keys, so archived tokens keep verifying. |
Every response other than 200 is Cache-Control: no-store. The JWKS is Cache-Control: public, max-age=600.
The token
Section titled “The token”The JOSE header:
{ "alg": "ES256", "kid": "trust-AbCdEf0123456789", "typ": "trust-attestation+jwt" }The claims:
| Claim | Value |
|---|---|
iss |
The installation’s origin, for example https://breeze.example.com |
sub |
The public page URL, for example https://breeze.example.com/portal/trust/northwind/acme |
iat |
When the token was signed (seconds since the epoch) |
exp |
The earlier of iat + 1 hour and lastEvaluatedAt + 24 hours |
jti |
A random UUID, unique per token |
trust.v |
1 |
trust.payload |
The exact public payload the page renders, including its snapshot id |
trust.lastEvaluatedAt |
When Breeze last evaluated the page’s controls (ISO 8601) |
There is no aud claim. The signature algorithm is always ES256 (ECDSA P-256 with SHA-256).
Verify a token
Section titled “Verify a token”A correct verifier checks all of the following, not only the signature:
- The signature, against a key from the installation’s
jwks.json, with the algorithm pinned toES256. typistrust-attestation+jwt.issis the installation’s origin that you expect.subis the page URL you expect.exphas not passed.
The examples below use https://breeze.example.com and the page northwind/acme.
Node.js (jose)
Section titled “Node.js (jose)”import { createRemoteJWKSet, jwtVerify } from 'jose';
const origin = 'https://breeze.example.com';const page = `${origin}/portal/trust/northwind/acme`;const jwks = createRemoteJWKSet(new URL(`${origin}/api/v1/trust/attestation/jwks.json`));
const token = await (await fetch(`${origin}/api/v1/trust/attestation/northwind/acme`)).text();const { payload } = await jwtVerify(token, jwks, { algorithms: ['ES256'], typ: 'trust-attestation+jwt', issuer: origin, subject: page,});console.log(payload.trust.lastEvaluatedAt, payload.trust.payload.summary);Python (PyJWT)
Section titled “Python (PyJWT)”import jwtimport requests
origin = "https://breeze.example.com"page = f"{origin}/portal/trust/northwind/acme"jwks = jwt.PyJWKClient(f"{origin}/api/v1/trust/attestation/jwks.json")
token = requests.get(f"{origin}/api/v1/trust/attestation/northwind/acme", timeout=10).textif jwt.get_unverified_header(token).get("typ") != "trust-attestation+jwt": raise ValueError("not a trust attestation")claims = jwt.decode( token, jwks.get_signing_key_from_jwt(token).key, algorithms=["ES256"], issuer=origin, options={"require": ["iss", "sub", "iat", "exp"]},)if claims["sub"] != page: raise ValueError("attestation is for a different page").NET (Microsoft.IdentityModel.JsonWebTokens)
Section titled “.NET (Microsoft.IdentityModel.JsonWebTokens)”using Microsoft.IdentityModel.JsonWebTokens;using Microsoft.IdentityModel.Tokens;
var origin = "https://breeze.example.com";var page = $"{origin}/portal/trust/northwind/acme";using var http = new HttpClient();var jwks = new JsonWebKeySet(await http.GetStringAsync($"{origin}/api/v1/trust/attestation/jwks.json"));var token = await http.GetStringAsync($"{origin}/api/v1/trust/attestation/northwind/acme");
var result = await new JsonWebTokenHandler().ValidateTokenAsync(token, new TokenValidationParameters{ IssuerSigningKeys = jwks.GetSigningKeys(), ValidAlgorithms = new[] { SecurityAlgorithms.EcdsaSha256 }, ValidTypes = new[] { "trust-attestation+jwt" }, ValidIssuer = origin, ValidateAudience = false, // the token has no aud claim ValidateLifetime = true,});if (!result.IsValid || (string)result.Claims["sub"] != page) throw new SecurityTokenException("attestation did not verify");Archived tokens
Section titled “Archived tokens”A token stays verifiable after it expires, as long as its key is still in the JWKS: keys that are rotated out remain published as verify-only. An expired token is a historical statement (“on this date, this installation computed this payload”), not a statement about now. To check an archived token, verify the signature, typ, iss and sub as above, and evaluate the dates as of the time it was issued instead of now. With jose:
import { decodeJwt, jwtVerify } from 'jose'; // plus `jwks`, `origin` and `page` from the Node example above
const { payload } = await jwtVerify(token, jwks, { algorithms: ['ES256'], typ: 'trust-attestation+jwt', issuer: origin, subject: page, currentDate: new Date(decodeJwt(token).iat * 1000), // dates checked as of issuance});For a current statement, fetch a new token. A key that was revoked after a compromise is removed from the JWKS, and tokens it signed no longer verify.
Self-hosting
Section titled “Self-hosting”There is nothing to set up. The installation creates its signing key the first time an attestation or the JWKS is requested, and stores the private key encrypted with APP_ENCRYPTION_KEY.
- Set
APP_ENCRYPTION_KEY_IDso the encrypted key is bound to its database column (the same setting protects every other encrypted column). - Set
DASHBOARD_URL(orPUBLIC_APP_URL) to the public URL of your installation. Theissandsubclaims come from it. In production, attestation answers503until one of them is set.
Rotating the signing key
Section titled “Rotating the signing key”Rotation is an operator task. The command runs from a Breeze source checkout (the API container image ships the compiled server only, not scripts/), with DATABASE_URL, REDIS_URL, APP_ENCRYPTION_KEY and APP_ENCRYPTION_KEY_ID set to the production values:
pnpm --filter @breeze/api trust:signing-key listpnpm --filter @breeze/api trust:signing-key prepare # new key, published in the JWKS, not signing yet# wait at least 10 minutes (the JWKS max-age) so caching verifiers have the new keypnpm --filter @breeze/api trust:signing-key activate <kid> # new key signs; the old key becomes verify-onlyactivate refuses a key prepared less than 10 minutes ago unless you pass --force. The previous key stays in the JWKS so tokens it signed keep verifying; its private key is destroyed.
If a key is compromised, revoke it and mint a replacement in one step:
pnpm --filter @breeze/api trust:signing-key revoke <kid> --replaceThe revoked key leaves the JWKS at once. Verifiers that cache the JWKS stop trusting it within 10 minutes. Without --replace, revoking the active key leaves the installation with no signing key, and attestation answers 503 until you prepare and activate a new one: Breeze never creates a replacement on its own after a revoke.
Every command prints the keyset (key ids, statuses and dates, never key material) and writes an audit log entry trust_signing_key.<command>. After a change it tells the running API instances to reload the keyset; if it cannot (for example, Redis is unreachable), it says so, and each instance picks up the change within 60 seconds. A revoke also deletes the cached tokens the revoked key signed. The command exits non-zero if a committed change could not be audited or a revoke could not be announced, and says which.