Four checks. BLAKE3, CIDv1, ed25519, JCS-canonical JSON. No network calls to our infrastructure required. The verification reduces to hashing bytes and verifying a signature against a public key you can read off the agent card.
This page is for auditors and operators who need to confirm that a Due Diligence Statement they received was actually issued by eudr.dev, has not been tampered with, and corresponds to a real emem-signed evidence set.
It assumes nothing about your toolchain. Anything you can do with a Rust binary you can also do with a Python notebook, a curl one-liner, or a printout and a calculator.
A signed receipt carries the exact bytes that were signed, base64- encoded, in a field called canonical_b64. The CID and the ed25519 signature are both computed over those bytes — not over the payload on its own, and not over the CID. Decode canonical_b64 once and every check below is a pure function of that byte string. payload is a human-readable view; you can recompute the bytes from it as a cross- check, but you never need to trust it.
A signed DDS receipt is a JSON object with this shape:
{
"schema": "eu.eudr.dds.v2025_2650+slim",
"cid": "bafkr4ih5mabbtoysc2odmszbnugm2jxj4twe7n3m7yk55rkhbeeozm6e2q",
"payload": { /* human-readable Annex II JSON */ },
"canonical_b64": "eyJwYXlsb2FkIjp7IjFfb3BlcmF0b3IiOnsiYWRk…MjY1MCtzbGltIn0=",
"signature_b64": "KEg/AmyCbo/VN7peZBw2bOnOMiHgnP848UHTYBFk…",
"signed_at": "2026-06-04T04:36:23.165878990Z",
"signer": {
"did": "did:web:eudr.dev",
"pubkey_b32": "rc6pbb6bxrfhpt4jr6yhmklkjubresa4kc4uofdxp33qncmpi2vq",
"pubkey_b64": "iLzwh8G8SnfPiY+wdilqTQMSSBxQuUcUd373BomPRqs="
}
}
Field notes (these matter — earlier drafts of this page got them wrong):
cid is a CIDv1 with the raw codec and a BLAKE3-256 multihash. Its multibase prefix is bafkr4i…, not the bafkrei…/bafybei… you see from IPFS's SHA-256 CIDs.canonical_b64 is base64 of the wrapper object {"payload": …, "schema": …} after JCS canonicalisation — not of payload alone.signature_b64 is ed25519 over those same canonical bytes.signer: signer.did, signer.pubkey_b32, signer.pubkey_b64. pubkey_b32 and pubkey_b64 are the same 32-byte ed25519 public key in two encodings.signed_at (RFC 3339, UTC).The four checks below prove, independently:
https://eudr.dev/.well-known/agent-card.json.You can stop at check #3 if you trust our published DID. Check #4 proves the evidence underneath. The deep audit case.
# Verifies the CID re-derivation and the signature in one shot. The CLI
# is a single statically-linked Rust binary; no network calls.
cargo install eudr-cli # or `cargo run --bin eudr` from this repo
eudr verify <receipt.json>
eudr verify prints the cid, the signer block, and "valid": true when the bytes are intact and the signature checks out — for example:
{
"cid": "bafkr4ih5mabbtoysc2odmszbnugm2jxj4twe7n3m7yk55rkhbeeozm6e2q",
"signer": {
"did": "did:web:eudr.dev",
"pubkey_b32": "rc6pbb6bxrfhpt4jr6yhmklkjubresa4kc4uofdxp33qncmpi2vq",
"pubkey_b64": "iLzwh8G8SnfPiY+wdilqTQMSSBxQuUcUd373BomPRqs="
},
"valid": true
}
There is also a browser verifier at /verify that runs the same checks client-side. The rest of this page is for people who want to re-implement the verification by hand.
Both the CID and the signature are taken over the canonical-JSON encoding of the wrapper object. The reference implementation lives in crates/eudr-receipts/src/lib.rs::canonical_json (write_canonical). It is ~40 lines; the rules are:
{"payload": <payload>, "schema": <schema>} — the receipt's top-level schema string plus its payload object, and nothing else. Mint builds exactly this object and canonicalises it."1_operator" sorts before "2_product" before "schema")., between elements and : between key and value, with nothing around them.serde_json's string serialiser: standard JSON escapes (\n, \t, \", \\, \uXXXX for control chars), and non-ASCII characters are kept as raw UTF-8, not \u-escaped (equivalent to Python's ensure_ascii=False). So é in "Soubré" is two UTF-8 bytes in the signed stream, not é.serde_json::Number::to_string. Integers have no decimal point or exponent; a value that was a JSON float keeps its fractional form — e.g. 4800.0 stays 4800.0, it does not collapse to 4800. (This is why the receipt ships canonical_b64: it pins the exact float spelling so a re-serialisation can't drift.)In Python the wrapper-object encoding reproduces byte-for-byte with:
json.dumps({"payload": payload, "schema": schema},
sort_keys=True, separators=(",", ":"),
ensure_ascii=False).encode("utf-8")
This was checked against a real receipt: the recompute is identical to base64.b64decode(canonical_b64), all 74021 bytes. See the executed results at the bottom of this page.
One caveat on the float rule:
json.dumpsreproduces4800.0because the value arrived as a JSON float and Python'sreprof that float happens to matchserde_json's. For the receipts this engine emits that holds, and thecanonical_b64cross-check below will catch any divergence before you rely on it. If you only need certainty and not a from-scratch recompute, skip straight to decodingcanonical_b64— that is the authoritative byte string.
The CID is a deterministic hash of the canonical bytes. Re-hash those bytes the way the engine did and you get the same CID, which proves the signed content is unchanged.
Algorithm, exactly:
canon = base64-decode(canonical_b64) — the signed bytes.digest = BLAKE3-256(canon) — 32 bytes.| byte | meaning |
|---|---|
0x01 | CID version 1 |
0x55 | multicodec raw |
0x1e | multihash code blake3 (0x1e) |
0x20 | multihash digest length = 32 bytes |
| … | the 32 BLAKE3-256 digest bytes |
i.e. cid_bin = bytes([0x01, 0x55, 0x1e, 0x20]) + digest (36 bytes).
b. The result starts bafkr4i… and must equal the receipt's cid.import base64
from blake3 import blake3 # pip package "blake3"
canon = base64.b64decode(r["canonical_b64"])
digest = blake3(canon).digest() # 32 bytes
cid_bin = bytes([0x01, 0x55, 0x1e, 0x20]) + digest # CIDv1 raw + blake3-256
b32 = base64.b32encode(cid_bin).decode().rstrip("=").lower()
recomputed = "b" + b32
assert recomputed == r["cid"], f"CID mismatch: {recomputed} != {r['cid']}"
print("CID check passed ✓")
If the CID matches, the signed bytes are exactly what was hashed. Change any byte — even a stray space inside payload — and re-deriving from those altered bytes yields a different CID.
signature_b64 is an ed25519 signature over the canonical bytes (the decoded canonical_b64) — not over the CID, and not over payload directly. Verify it against signer.pubkey_b64.
import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
canon = base64.b64decode(r["canonical_b64"]) # the signed bytes
pub = base64.b64decode(r["signer"]["pubkey_b64"]) # 32-byte ed25519 key
sig = base64.b64decode(r["signature_b64"]) # 64-byte signature
Ed25519PublicKey.from_public_bytes(pub).verify(sig, canon) # raises on failure
print("Signature check passed ✓")
signer.pubkey_b32 is the same public key, encoded differently: RFC 4648 base32, lowercase, no padding (the engine produces it as BASE32_NOPAD.encode(pubkey).to_lowercase()). You can confirm the two encodings agree, which is a cheap guard against a swapped key:
assert base64.b32encode(pub).decode().rstrip("=").lower() \
== r["signer"]["pubkey_b32"]
If the signature verifies, the holder of the private key signed these bytes. The only entity holding that key is the engine operator (VORTX AI PRIVATE LIMITED for the hosted eudr.dev).
import json, base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
with open("receipt.json") as f:
r = json.load(f)
# (a) The exact signed bytes.
canon = base64.b64decode(r["canonical_b64"])
# (b) OPTIONAL cross-check: re-derive the canonical bytes from {payload, schema}.
# Must equal `canon`. (See the float caveat above.)
body = {"payload": r["payload"], "schema": r["schema"]}
recomputed = json.dumps(body, sort_keys=True, separators=(",", ":"),
ensure_ascii=False).encode("utf-8")
assert recomputed == canon, "canonical-bytes cross-check failed"
# (c) CID = multibase-base32lower( 0x01 0x55 0x1e 0x20 || BLAKE3-256(canon) ).
try:
from blake3 import blake3
digest = blake3(canon).digest()
cid_bin = bytes([0x01, 0x55, 0x1e, 0x20]) + digest
cid = "b" + base64.b32encode(cid_bin).decode().rstrip("=").lower()
assert cid == r["cid"], f"CID mismatch: {cid} != {r['cid']}"
print("CID check passed ✓")
except ImportError:
print("CID check skipped: install the `blake3` package, or run `eudr verify`.")
# (d) ed25519 over the canonical bytes, against signer.pubkey_b64.
pub = base64.b64decode(r["signer"]["pubkey_b64"])
sig = base64.b64decode(r["signature_b64"])
Ed25519PublicKey.from_public_bytes(pub).verify(sig, canon)
assert base64.b32encode(pub).decode().rstrip("=").lower() \
== r["signer"]["pubkey_b32"]
print("Signature check passed ✓")
A valid signature is only meaningful if you know whose key you just trusted. The signer's identity is published at the agent card:
curl -sS https://eudr.dev/.well-known/agent-card.json | jq '.identity'
You expect output like:
{
"did": "did:web:eudr.dev",
"pubkey_b32": "rc6pbb6bxrfhpt4jr6yhmklkjubresa4kc4uofdxp33qncmpi2vq",
"pubkey_b64": "iLzwh8G8SnfPiY+wdilqTQMSSBxQuUcUd373BomPRqs="
}
Confirm:
identity.did matches the signer.did in your receipt.identity.pubkey_b64 (and/or pubkey_b32) matches the same field under signer in your receipt — and is the key the signature in Check 2 verified against.If they match, the receipt was signed by the entity that publishes the agent card at https://eudr.dev. The DID is did:web:eudr.dev, which by the DID Web specification ties the identity to control over that domain's TLS certificate + its agent-card document.
The receipt's signer.did should always be did:web:eudr.dev. A receipt that carries any other DID was not issued by this service.
This is the deep audit. The receipt's payload carries the per-cell fact CIDs from emem.dev. Each CID dereferences to a content-addressed fact at emem.dev.
For each cell CID:
curl -sS https://emem.dev/v1/facts/<cid> > fact.json
Then apply the same verification to the emem fact:
canonical_b64 (same BLAKE3 + CIDv1-raw algorithm as Check 1).https://emem.dev/.well-known/agent-card.json.The two cryptographic layers are independent: even if our DID rotated tomorrow, the emem facts under a historical receipt remain verifiable from emem.dev's side. That's the value of content-addressed evidence — it survives identity changes.
You have proven, without trusting our infrastructure for anything other than DNS + TLS on the eudr.dev domain:
What you have NOT proven:
This is the same evidentiary boundary every chain-of-custody audit faces. The engine narrows the trust footprint as much as cryptography allows.
Run eudr verify <receipt.json>. If it prints "valid": true, the bytes are what was signed and the signature is from the DID under signer. Cross-check that DID against the deployment that they say issued it.
You can. eudr verify is fully offline. The agent-card lookup (Check 3) is the only network step; do it once, cache the pubkey, and use it forever. CID and signature verification need no network at all.
The signed bytes you hashed are not the ones that were signed. Possible causes:
payload alone instead of the decoded canonical_b64 (the wrapper {"payload", "schema"} bytes). Hash canonical_b64.bafkr4i….canonical_b64 and re-ordered keys without canonicalising.Run eudr verify first; it is the reference implementation. If the CLI passes but your re-implementation fails, your canonicalisation or CID layout has drifted.
Either the canonical bytes changed, or the signature and key are from a different pair. Make sure you are verifying over the decoded canonical_b64 (not the CID, not payload) and against signer.pubkey_b64. If it still fails, the receipt is tampered with or was never genuine.
If a receipt you received from us fails to verify, please email avijeet@vortx.ai with:
We will respond within 72 hours. A failing verification on a hosted eudr.dev receipt is a security-grade incident, not a support ticket; we treat it accordingly.
The recipe on this page was checked against a live receipt (schema: eu.eudr.dds.v2025_2650+slim, cid: bafkr4ih5mabbtoysc2odmszbnugm2jxj4twe7n3m7yk55rkhbeeozm6e2q) on a host with Python cryptography 46.0.5 but without a blake3 module. Observed results:
json.dumps({"payload": payload, "schema": schema}, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") produced 74021 bytes that are byte-for-byte identical to base64.b64decode(canonical_b64).Ed25519PublicKey. from_public_bytes(pubkey_b64).verify(signature_b64, canon) passed. Verifying the same signature over the CID bytes failed — a direct demonstration that the signature is over the canonical bytes, not the CID.base32(pubkey).lower() without padding equals signer.pubkey_b32.blake3 module; nothing was installed). It was confirmed instead by the reference binary eudr verify, which independently re-derived bafkr4ih5mab… from canonical_b64 and reported "valid": true, and by source-reading cid_blake3_raw / Cid::new_v1(0x55, blake3(canon)) in crates/eudr-receipts/src/lib.rs.crates/eudr-receipts/src/lib.rs/verify