# jwt attestor

Source: https://www.testifysec.com/docs/cilock/attestors/jwt

The cilock jwt attestor parses a JWT, fetches a JWKS, verifies the token signature, and signs the decoded claims plus the verifying key into in-toto evidence as a generic OIDC identity proof.

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

 

| Name | `jwt` |
| --- | --- |
| Predicate type | `https://aflock.ai/attestations/jwt/v0.1` |
| Lifecycle | `prematerial` |
| Default binary? | No |
| Recommended trace | off — no syscall tracing needed |
| Auto-attaches when | *Not 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`](https://www.testifysec.com/docs/cilock/attestors/github), [`gitlab`](https://www.testifysec.com/docs/cilock/attestors/gitlab), and [`gcp-iit`](https://www.testifysec.com/docs/cilock/attestors/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

 

```json
{
  "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](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

 

- [Catalog row](https://www.testifysec.com/docs/cilock/reference/attestor-catalog)
 - [`github`](https://www.testifysec.com/docs/cilock/attestors/github), [`gitlab`](https://www.testifysec.com/docs/cilock/attestors/gitlab), [`gcp-iit`](https://www.testifysec.com/docs/cilock/attestors/gcp-iit) — all embed this attestor
 - Upstream: [witness/jwt.md](https://github.com/in-toto/witness/blob/main/docs/attestors/jwt.md)

 

---

**This page is generated from the CI/lock tool catalog.** Don't edit it here — the source is [`attestation/detection/docs/jwt.doc.md`](https://github.com/aflock-ai/rookery/edit/main/attestation/detection/docs/jwt.doc.md) in [aflock-ai/rookery](https://github.com/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 →](https://github.com/aflock-ai/rookery/edit/main/attestation/detection/docs/jwt.doc.md) ·  [Add a new tool →](https://github.com/aflock-ai/rookery/new/main/attestation/detection/docs)

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.

Reference generated from the product documentation. Match commands and support details to your installed release.
