---
title: "Signed Passports: Verified Outside Our Own Stack"
description: "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."
canonical_url: "https://openepcis.io/docs/digital-product-passport/signed-passports"
last_updated: "2026-08-26T20:11:04.495Z"
---

## 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](/docs/digital-product-passport/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:

```text
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.

<table>
<thead>
  <tr>
    <th>
      Scenario
    </th>
    
    <th>
      What is presented
    </th>
    
    <th>
      What it replaces
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Onboarding an economic operator
    </td>
    
    <td>
      A legal-person attestation from an organisational wallet
    </td>
    
    <td>
      A self-asserted company name on an account form
    </td>
  </tr>
  
  <tr>
    <td>
      Acting on behalf of a brand
    </td>
    
    <td>
      A mandate / power-of-representation attestation
    </td>
    
    <td>
      A support ticket and a manual role grant
    </td>
  </tr>
  
  <tr>
    <td>
      Proving prefix entitlement
    </td>
    
    <td>
      The brand owner's GS1 prefix licence credential
    </td>
    
    <td>
      Trusting that the GTINs written are the ones licensed
    </td>
  </tr>
  
  <tr>
    <td>
      Unlocking a restricted access tier
    </td>
    
    <td>
      A credential showing the holder is an authorised treatment or repair operator
    </td>
    
    <td>
      Provisioning an account per facility, per country
    </td>
  </tr>
</tbody>
</table>

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](https://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:

```bash
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:

```bash
curl -s -H 'Accept: application/linkset+json' \
  https://id.example.com/01/09521002005004
```

## Next steps

- [DPP Resolution Flow](/docs/digital-product-passport/dpp-resolution-flow) — how
a scanned QR code reaches a typed passport response in the first place
- [Standards Interoperability](/docs/digital-product-passport/interoperability) —
the vocabulary side of the same question
- [`VC_INTEROPERABILITY.md`](https://github.com/openepcis/openepcis-dpp-ready/blob/main/extensions/common/interop/docs/VC_INTEROPERABILITY.md)
— the engineering evidence page, layer by layer, with the measurements behind
everything summarised here
