Independent AI agent for EU Deforestation Regulation compliance, not affiliated with the EU. Scope & disclaimer →
eudr.dev

Verifying a Due Diligence Statement offline

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.

The one rule that makes this work

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.

What you are about to verify

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):

The four checks below prove, independently:

  1. The signed bytes (and therefore the Annex II payload) have not been tampered with since signing.
  2. The signature was produced by the holder of the corresponding private key.
  3. The signing identity is the one advertised at https://eudr.dev/.well-known/agent-card.json.
  4. The upstream Earth-observation evidence (the per-cell facts from emem.dev) is itself genuine and unchanged.

You can stop at check #3 if you trust our published DID. Check #4 proves the evidence underneath. The deep audit case.

The zero-code path

# 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.

The canonical form

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:

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.dumps reproduces 4800.0 because the value arrived as a JSON float and Python's repr of that float happens to match serde_json's. For the receipts this engine emits that holds, and the canonical_b64 cross-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 decoding canonical_b64 — that is the authoritative byte string.

Check 1. Re-derive the CID

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:

  1. canon = base64-decode(canonical_b64) — the signed bytes.
  2. digest = BLAKE3-256(canon) — 32 bytes.
  3. Prepend the CIDv1 + multihash header bytes:
bytemeaning
0x01CID version 1
0x55multicodec raw
0x1emultihash code blake3 (0x1e)
0x20multihash digest length = 32 bytes
the 32 BLAKE3-256 digest bytes

i.e. cid_bin = bytes([0x01, 0x55, 0x1e, 0x20]) + digest (36 bytes).

  1. Multibase-encode with base32: lowercase RFC 4648 alphabet, no padding, and prepend the multibase tag 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.

Check 2. Verify the ed25519 signature

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).

Full corrected snippet

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 ✓")

Check 3. Confirm the signing identity

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:

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.

Check 4. Verify the underlying Earth-observation evidence

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:

  1. Re-derive the CID from the fact's canonical_b64 (same BLAKE3 + CIDv1-raw algorithm as Check 1).
  2. Verify the ed25519 signature over the fact's canonical bytes.
  3. Confirm the pubkey against emem's agent card at https://emem.dev/.well-known/agent-card.json.
  4. Inspect the band data — Hansen loss-year, ESA WorldCover class, JRC GFC2020 binary — and confirm the verdict logic in our methodology gives the same per-cell verdict.

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.

What you have when all four checks pass

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.

Practical scenarios

"An auditor I trust sent me this DDS. Should I trust it?"

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.

"I want to verify without internet access."

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 CID doesn't match."

The signed bytes you hashed are not the ones that were signed. Possible causes:

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.

"The signature doesn't verify."

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.

When to escalate

If a receipt you received from us fails to verify, please email avijeet@vortx.ai with:

  1. The full receipt JSON.
  2. The verification command you ran + its output.
  3. The version of any third-party verification tool you used.

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.

Appendix: what was actually executed against a real receipt

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:

See also