Do not trust us. Run the verifier yourself.
Security pages are full of reassuring words: audited, encrypted, trusted. They may describe real work. From the reader's side, they are still claims made by the party that benefits from them.
R-Series starts from a stricter premise. The strongest assurance we can offer is not a statement about our own controls. It is evidence that you can check on your own machine, without asking us anything.
That is what the verifier is for. This post walks through exactly what it checks, in order, so you know what a pass means and what a fail means.
Offline by design
An R-Series receipt can be verified with two inputs: the receipt and the issuer's public keys. The verifier does not call our servers, does not need an account and does not look anything up in our database. If it did, you would be trusting us again.
The checks, in order
Each step must pass before the next one runs. Any failure stops verification and the receipt is rejected.
- Parse. The receipt must be well-formed JSON with every required field present and of the right type. Anything malformed is rejected rather than guessed at.
- Canonical bytes. The signed content is re-serialised using RFC 8785, the JSON Canonicalization Scheme. This produces one exact byte sequence regardless of key order, whitespace or number formatting in the file you were given. Every implementation must arrive at the same bytes.
- Digest. The verifier computes a SHA-256 digest over those canonical bytes and compares it with the digest the receipt carries. A single changed byte anywhere in the signed content produces a different digest.
- Ed25519. The classical signature over the digest is checked against the issuer's Ed25519 public key, as specified in RFC 8032.
- ML-DSA-65. The post-quantum signature over the same digest is checked against the issuer's ML-DSA-65 public key, as specified in NIST FIPS 204.
- Both legs present. The verifier confirms that both signatures were present and both verified. A receipt carrying only one leg is not a weaker pass. It is a fail. This prevents a downgrade, where an attacker strips the signature they cannot forge and hopes the verifier settles for the other.
- Chain. Each receipt names the SHA-256 digest of the receipt before it. When you verify a sequence, the verifier recomputes each predecessor's digest and checks the link. A deleted, inserted or reordered receipt breaks the chain at that point, and the verifier reports where.
Only when all seven pass is the receipt reported as verified.
Why the order matters
Signatures are checked over canonical bytes, never over the raw file. If a verifier signed or checked whatever bytes happened to arrive, two honest implementations could disagree about a receipt that had not been tampered with, and a careful attacker could exploit the gap. Canonicalising first means every verifier is asking the same question about the same bytes.
What a pass does and does not tell you
A pass tells you that the holder of the issuer's keys signed exactly this content, with both algorithms, and that it sits in the chain where it claims to. It does not tell you that the action was wise, that the content was true, or that the keys were well protected. Those are separate questions.
Try to make it fail
The most useful thing you can do with a verifier is attack it. The in-browser demo makes a fresh key pair on your machine, signs a sample receipt with both Ed25519 and ML-DSA-65, and runs the first six checks in front of you. Nothing is sent to a server. Then it lets you break things:
- Change one byte of the receipt and watch verification fail.
- Strip the post-quantum signature and watch the receipt fail rather than pass on Ed25519 alone.
- Make a new receipt with new keys and run it again.
The chain check needs a sequence of receipts rather than one, so the single-receipt demo in your browser does not run it.
If you ever find a receipt that verifies when it should not, that is the most valuable result you could send us.
One verifier is not enough
A verifier written by the same team as the signer can share its blind spots. That is why R-Series has three verifiers, written separately in JavaScript, Python and Go, which are run over the same parity cases and must agree on every one. We will write about that separately.
For the full picture of how receipts are built and checked, read how proof works. Then go and try to break it.