Verification

A receipt's signature lets you prove, years later, that the receipt bytes you stored are the bytes the service produced. Verification needs no trust in us - only the two public key channels:

  1. GET https://nanotoll.dev/.well-known/nanotoll-verify.json - the public key and its key_id (see key distribution).
  2. A DNS TXT record on _nanotoll-verify.nanotoll.dev carrying the SHA-256 fingerprint of that key.

The check

The example receipt in POST /v1/compress verifies with exactly this snippet - save it as receipt.json and run:

import hashlib, json, urllib.request
from nacl.signing import VerifyKey

receipt = json.load(open("receipt.json"))  # the full API response receipt

wk = json.load(urllib.request.urlopen(
    "https://nanotoll.dev/.well-known/nanotoll-verify.json"))
key_id = receipt["key_id"]
entry = next(k for k in wk["keys"] if k["key_id"] == key_id)

key = bytes.fromhex(entry["public_key"])
# Cross-check against the DNS TXT fingerprint:
#   dig TXT _nanotoll-verify.nanotoll.dev +short
# must equal sha256(key) hex.
print("sha256(key):", hashlib.sha256(key).hexdigest())

sig = bytes.fromhex(receipt.pop("receipt_signature"))
receipt.pop("key_id")

# Canonical form: the received values, recursively key-sorted, compact.
# Serialize exactly what you received - do not reformat or round numbers.
payload = json.dumps(receipt,
                     sort_keys=True, separators=(",", ":")).encode()

VerifyKey(key).verify(payload, sig)
print("signature OK")

Run against the worked-example receipt on the compress page, this prints signature OK: the example is a real captured response, not an illustration.

What a key_id change means

Receipts embed the key_id that signed them. Store the key_id alongside your receipts - it is part of the record, and it is what you check against the well-known file when you verify. If a receipt carries a key_id that is not in the well-known file, treat the receipt as unverified.