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-responseresponse_hashwhen 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 whenhash_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:
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:
--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.