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.

ScenarioWhat is presentedWhat it replaces
Onboarding an economic operatorA legal-person attestation from an organisational walletA self-asserted company name on an account form
Acting on behalf of a brandA mandate / power-of-representation attestationA support ticket and a manual role grant
Proving prefix entitlementThe brand owner's GS1 prefix licence credentialTrusting that the GTINs written are the ones licensed
Unlocking a restricted access tierA credential showing the holder is an authorised treatment or repair operatorProvisioning 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

Last updated: