Signed Passports: Verified Outside Our Own Stack
How OpenEPCIS signs product passports so that software nobody here wrote can verify them — W3C Data Integrity, did:web, and the canonicalization layer that decides whether a signature travels.
What this page is
A digital product passport is worth exactly as much as the confidence that nobody altered it. That confidence comes from a signature — and a signature is only worth something if someone else's software can check it. Anyone can sign. The question is whether a regulator, a recycler or a customs authority can verify without installing anything of ours.
This page describes how OpenEPCIS signs passports, and why the hardest part of getting a signature to travel is not the cryptography.
It sits alongside Standards Interoperability, which covers vocabulary alignment — whether our terms line up with UNTP, CIRPASS-2 and ESPR. This page is about cryptographic interoperability: same word, different question.
Demonstrated in both directions
On the OpenEPCIS reference deployment, both directions have been exercised against implementations written by other people.
Reading. A real GS1 licence credential, fetched from GS1's live service and checked against GS1's own published key document, verifies in the OpenEPCIS engine — and stops verifying the moment a single byte changes. GS1 is the root of trust for GS1 Digital Link, and no part of their issuing stack is ours.
Writing. A battery passport credential issued by OpenEPCIS is accepted by an
independent third-party verifier running on an unrelated library stack, which
reports the proof as ecdsa-rdfc-2019 | verified: true.
The second one is the harder claim, and it is the one most implementations cannot make. Signing is easy. Being verifiable by a stranger is the point.
How it is put together
W3C Data Integrity, ecdsa-rdfc-2019. The proof covers the passport as a
graph rather than as a byte stream, so it survives reserialisation: reformat the
JSON, reorder the keys, convert to another RDF syntax, and the proof still holds.
A passport that has passed through three systems is still the passport that was
signed.
did:web for the issuer key. A verifier resolves the key over ordinary
HTTPS from a domain the issuer controls. No registry to join and no consortium
to be admitted to: a verifier needs the published contexts and the DID document,
both public, and nothing else from us.
The layer that decides whether a signature travels
This is the part worth understanding, because it is where cross-vendor verification is won or lost, and it stays invisible until it bites.
Parts of a passport carry no identifier of their own: the individual material entries, the hazard statements, the footprint figures. Before signing, they must be numbered so that any two readers see them in the same order. That step is called canonicalization, it is specified exactly, and the signature is a hash over its result.
Two implementations that number them differently produce different bytes from identical data — and the verifier reports an invalid signature although nothing was tampered with. The whole failure mode fits in four lines. Two nodes pointing at a third is the classic case where the numbering rules decide:
one implementation the standard
#test #A _:c14n0 #test #A _:c14n2
#test #B _:c14n1 #test #B _:c14n0
_:c14n0 #next _:c14n2 _:c14n0 #next _:c14n1
_:c14n1 #next _:c14n2 _:c14n2 #next _:c14n1
Same graph, different labels, and a signature that verifies nowhere but at home. On a real battery passport with 92 unnamed parts there is no prospect of accidental agreement.
OpenEPCIS therefore holds its signing path to the W3C rdf-canon conformance
suite in the build, rather than to its own round-trip, and pins the canonical
output of a fixed example across both runtimes it uses. Conformance at this layer
is a test result here, not an assumption — and that is why the signatures verify
elsewhere.
Where wallets fit
There is an intuitive assumption worth correcting, because it sends implementations down the wrong path: that a product passport is something a wallet carries.
It usually is not. A passport is an assertion about a thing, published so that anyone scanning the product can read it — not a personal attestation a holder presents about themselves. The credential formats a wallet must accept (IETF SD-JWT VC and ISO/IEC 18013-5 mdoc are the two the EU wallet ecosystem is built around) matter when a holder is in the picture. A wallet accepts a format, never "a Verifiable Credential" in the abstract, so format is the lever — which is why OpenEPCIS issues Data Integrity for machine-to-machine verification and UNTP alignment, and SD-JWT VC where a holder is involved.
The more interesting direction is the reverse one: wallets as how the actors prove who they are and what they may see.
| Scenario | What is presented | What it replaces |
|---|---|---|
| Onboarding an economic operator | A legal-person attestation from an organisational wallet | A self-asserted company name on an account form |
| Acting on behalf of a brand | A mandate / power-of-representation attestation | A support ticket and a manual role grant |
| Proving prefix entitlement | The brand owner's GS1 prefix licence credential | Trusting that the GTINs written are the ones licensed |
| Unlocking a restricted access tier | A credential showing the holder is an authorised treatment or repair operator | Provisioning an account per facility, per country |
The last row is the one to watch, because it is ESPR Article 9 followed through. Access tiers exist today and are enforced at the data layer; what a presented credential adds is that the entitlement travels with the holder instead of being administered by us. A recycler in another member state proves what they are and reads what that entitles them to — no account, no onboarding call.
Two of the four pieces are already in place: the passport side is signed and independently verifiable, and GS1 licence credentials are verified today, including the delegation chain. What the scenarios above need in addition is the presentation exchange — which is why credential presentation rather than credential issuance is the part of the wallet story worth building next.
Why the published contexts are part of the signature
One consequence of signing a graph is worth stating plainly, because it shapes how the reference site is operated.
The translation into canonical form is driven by the JSON-LD context documents published on ref.openepcis.io. Every credential names them by URL, and a verifier fetches those URLs and repeats the translation. If what is served differs from what the issuer used — even by one term definition — the verifier reconstructs different bytes and rejects a perfectly good passport.
So those documents are not documentation. They are an input to the cryptography. Published versions are treated as records rather than as something to quietly correct, and the issuer and the reference site are kept in step deliberately.
Checking a credential yourself
Nothing here has to be taken on trust. A credential is publicly dereferenceable, and public verifier APIs exist that share none of our code — substitute the issuer host of the deployment you are looking at and a verifier you trust:
curl -s https://issuer.example.com/vc/credentials/<credential-id> \
| python3 -c 'import json,sys;print(json.dumps([json.load(sys.stdin)]))' \
| curl -s -X POST -H 'Content-Type: application/json' --data @- \
https://verifier.example.org/api/verify
The credential id sits in the gs1:dpp entry of the product's linkset, next to
the passport API itself:
curl -s -H 'Accept: application/linkset+json' \
https://id.example.com/01/09521002005004
Next steps
- DPP Resolution Flow — how a scanned QR code reaches a typed passport response in the first place
- Standards Interoperability — the vocabulary side of the same question
VC_INTEROPERABILITY.md— the engineering evidence page, layer by layer, with the measurements behind everything summarised here