Skip to main content

jwt attestor

Parses a JWT, fetches a JWKS, verifies the token's signature, and records the decoded claims plus the JWK that verified it.

Namejwt
Predicate typehttps://aflock.ai/attestations/jwt/v0.1
Lifecycleprematerial
Default binary?No
Recommended traceoff — no syscall tracing needed
Auto-attaches whenNot auto-detected — attach explicitly with -a.

The facts in this box are generated from the CI/lock binary's own catalog (cilock tools list). Do not hand-edit — run npm run gen:catalog.

What it captures​

The struct exposes two top-level JSON-tagged fields:

  • claims — the full set of decoded JWT claims, populated by go-jose's parsed.Claims(jwks, &a.Claims) after signature verification. Stored as map[string]interface{}, so every claim in the token (standard registered claims plus any custom issuer-specific claims) lands here verbatim.
  • verifiedBy (omitempty) — the JWKS coordinates that verified the signature:
    • jwksUrl — the URL the JWKS was fetched from, with any userinfo in it recorded as ****** (the rule the environment attestor applies to its values). The fetch itself uses the URL as configured, so an endpoint behind basic auth still verifies.
    • jwk — the jose.JSONWebKey (kid, kty, alg, public key material, etc.) whose kid matched the token header.

The token itself and the configured JWKS URL are kept as unexported fields (token, jwksUrl) and are deliberately not serialized.

This attestor is also embedded inside the github, gitlab, and gcp-iit attestors, which construct it via jwt.New(jwt.WithToken(...), jwt.WithJWKSUrl(...)) to share JWT parsing, JWKS fetch, and signature-verification logic.

When to use​

Use jwt directly when capturing an OIDC ID token from a CI platform that does not yet have a dedicated rookery attestor (the dedicated ones — GitHub Actions, GitLab CI, GCP — embed this attestor for you). It is a generic "I held this signed token and verified it against this JWKS" attestation.

Flags​

None. The attestor is configured programmatically through functional options (WithToken, WithJWKSUrl) by its embedders; it does not register CLI flags.

Output shape​

{
"claims": {
"iss": "https://token.actions.githubusercontent.com",
"sub": "repo:example/repo:ref:refs/heads/main",
"aud": "witness",
"exp": 1700000000,
"iat": 1699996400
},
"verifiedBy": {
"jwksUrl": "https://token.actions.githubusercontent.com/.well-known/jwks",
"jwk": {
"use": "sig",
"kty": "RSA",
"kid": "...",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
}
}

Gotchas​

  • Empty token fails fast. Attest() returns ErrInvalidToken("") if no token was supplied via WithToken; no network call is made.
  • JWKS fetch is hard-capped. The fetch uses a *http.Client with a 30 s timeout, requires HTTP 200 (anything else errors with unexpected status code from JWKS endpoint ...), and the response body is wrapped in io.LimitReader(resp.Body, 1<<20) — a malicious or misconfigured JWKS endpoint cannot OOM the attestor; oversized responses fail at JSON decode.
  • Fetch errors do not quote a credential. An error names the endpoint with its userinfo as ******. The error net/http wraps is text taken from the endpoint and from any redirect, so it is kept only when the request was not redirected, the text holds no @, and the URL holds no credential or Go dials the host the redacted endpoint still names; otherwise the error says only what kind of failure it was (the URL does not parse, the request timed out, the request failed). https://ci:secret/part@host/keys, whose password holds a /, fails to parse with invalid port ":secret", and a redirect to http://ci:secret@host:bad/keys fails with that Location quoted whole; both are withheld.
  • Bad signature fails the attestation. Signature verification is performed by parsed.Claims(jwks, &a.Claims). If the token's signature does not validate against any key in the fetched JWKS, Attest() returns error parsing claims: ... and the attestation does not run. The claims field is only populated on successful verification.
  • Missing kid is tolerated for verifiedBy only. After successful claims verification, the attestor walks the token headers for the first non-empty kid and looks it up in the JWKS. If no key matches, Attest() returns nil with verifiedBy left zero-valued — claims are still captured, but the recorded JWK is absent. (Signature verification itself has already succeeded by this point; this lookup is purely to record which JWK matched.)
  • No revocation, no expiration check, no audience check. This attestor records what the token says; it does not enforce exp, nbf, aud, or issuer constraints. Policy evaluation is responsible for asserting on claims.*.

CLI example​

See the constraint summary + reproduction recipe at https://github.com/aflock-ai/attestor-compliance-examples/tree/main/27-jwt. This attestor is currently blocked or doc-only — the linked example explains why and shows the recipe to validate once the constraint is removed.

See also​


This page is generated from the CI/lock tool catalog. Don't edit it here — the source is attestation/detection/docs/jwt.doc.md in aflock-ai/rookery. The same catalog powers cilock tools show jwt in the CLI, so a single edit keeps the terminal and the website in sync.

Edit this page →  ·  Add a new tool →

When you open the pull request: if you are an AI agent, add the ai-agent label. If you are a human, add the human label.