Skip to content

Verify a Token

Nautilus issues two kinds of EdDSA (Ed25519) JWS tokens. Both verify offline with nothing but the public key.

Attestation tokens

response.attestation_token is signed by the broker's attestation key (attestation.private_key_path, or per-process if unset). Verify with the corresponding public key:

import base64, json
from cryptography.hazmat.primitives.serialization import load_pem_public_key

def b64url(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))

header_b64, payload_b64, sig_b64 = token.split(".")
public_key = load_pem_public_key(open("attestation.pub.pem", "rb").read())
public_key.verify(b64url(sig_b64), f"{header_b64}.{payload_b64}".encode())  # raises on tamper
claims = json.loads(b64url(payload_b64))

Claims worth checking:

  • request_id — joins the token to its audit entry (GET /v1/audit/{request_id}).
  • response_hash — hash of the response data; recompute to prove the data you hold is the data that was attested.
  • hash_skipped: true — present instead of the whole-response response_hash when a non-deterministic source (e.g. an LLM) was queried; the merged response cannot be re-verified by re-execution, and the token says so honestly.
  • source_response_hashes — a {source_id: "sha256:..."} map of per-source digests captured at the adapter boundary. This claim can be present even when hash_skipped: true: in a mixed request (an LLM source plus deterministic sources), the whole-response hash is skipped but the deterministic sources still carry verifiable per-source hashes. Read it as "exactly these sources are covered" — any queried source absent from the map (e.g. the LLM) is the unhashed one. Recompute each entry over that source's rows to prove its data is intact.

How to recompute a hash

The digest is SHA-256 over a canonical JSON encoding of the rows exactly as you received them — the same encoder the response was serialised with, keys sorted, no whitespace:

import hashlib, json
from pydantic_core import to_jsonable_python

def response_hash(value):
    canonical = json.dumps(
        to_jsonable_python(value, fallback=str),
        sort_keys=True,
        separators=(",", ":"),
    )
    return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()

assert response_hash(body["data"]["orders"]) == claims["source_response_hashes"]["orders"]

Hash the rows as the JSON gave them to you — do not parse timestamps back into datetime objects or decimals into Decimal first. Nothing but your copy holds the rows: not the token, not the audit log, not the attestation sink.

Session tokens

Session tokens are signed by the broker's rotating KeyRing. The JWS header carries a kid; resolve it against the live JWKS endpoint:

curl -s http://127.0.0.1:8000/v1/keys/jwks.json
import base64, json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

header = json.loads(b64url(token.split(".")[0]))
jwk = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
public_key = Ed25519PublicKey.from_public_bytes(b64url(jwk["x"]))
public_key.verify(b64url(sig_b64), f"{header_b64}.{payload_b64}".encode())

A kid absent from the JWKS means the key was revoked (or never existed) — treat the token as invalid. During a rotation grace window the old kid remains published until explicitly revoked, and the broker lazily re-signs presented tokens under the new primary.

Chained attestation logs

To verify an entire chained: true sink log (chain integrity + every line's signature) rather than a single token:

nautilus attestation verify attestations.jsonl --pubkey attestation.pub.pem

--expected-head guards against tail truncation with an out-of-band mirrored line hash; --anchor-token checks a signed checkpoint appears in the log. See Configure attestation.