EUidentity — demo stack
Who writes what on the ledger, and when
The ledger holds one contract, EUidentityRegistry. Only the registry service has an account that signs transactions. Issuer and TSA never touch the ledger themselves: they ask the registry over HTTP to record something. Only organisations and hashes go on the ledger, never persons.
Overview
flowchart LR U(["You: dashboard or phone"]) --> H[hub / wallet] H -->|fetch PID| I[issuer] H -->|request timestamp| T[TSA] H -->|check signature| V[verifier] I -->|"POST /did/register, once at startup"| R[registry] T -->|POST /timestamp/anchor| R V -.->|"GET /timestamp/:hash"| R V -.->|"GET status list"| I R ==>|"only writer: transaction"| L[(Besu ledger<br/>EUidentityRegistry)] R -.->|free view call| L
Thick arrow: write (transaction, block, event). Dotted: read, leaves no trace on the ledger.
What goes on the ledger
| What | When | Requester | Contract function · event | Notes |
|---|---|---|---|---|
| Issuer DID + public key | Once, when the issuer starts | issuer | registerDIDDIDRegistered | did:web:issuer.euidentity.nl. The issuer makes a new key on every start, so a restart is a key rotation. Issuing a PID writes nothing. |
| Document hash (timestamp) | When timestamping | TSA | anchorTimestampTimestampAnchored | Only the first time counts. The same hash again: a transaction, but no event. |
| Signing key of a person | Never | nobody | registerSignerKey | Persons sign with their own did:key; the public key is inside the DID. |
| Revoked credential | Never | nobody | revokeCredential | Revocation runs through the issuer's status list. See Revocation in the menu. |
Which identifiers belong to a PID
A PID carries several references. Only the issuer's DID ends up on the ledger. Everything that belongs to the person stays in the wallet or in the PID itself.
flowchart LR
subgraph PID["PID of one person (SD-JWT VC or mdoc)"]
iss["iss<br/>https://issuer.euidentity.nl"]
cnf["cnf.jwk / deviceKey<br/>holder public key"]
st["status.status_list<br/>idx + uri"]
cid["credentialId<br/>pid-…"]
end
iss --> IDID["did:web:issuer.euidentity.nl<br/>issuer DID, one per issuer"]
IDID ==> L[(Besu ledger)]
iss --> TL["EU Trusted List<br/>issuer certificate + key"]
cnf --> HK["holder key in the wallet<br/>one per person, never leaves it"]
HK --> DK["did:key:zDn…<br/>signing DID, derived from the key<br/>never on a ledger"]
st --> SL["status list at the issuer<br/>bit idx: 0 = valid, 1 = revoked"]
cid -.-> IR["issuer's own records<br/>demo only"]| Identifier | Identifies | Where it lives | On the ledger | Per person |
|---|---|---|---|---|
did:web:issuer.euidentity.nl | The issuer of the PID (iss) | Ledger and EU Trusted List | yes | No, one per issuer |
Holder key (cnf.jwk / deviceKey) | The wallet the PID is bound to | Inside the PID; private key in the wallet's secure element | no | Yes |
did:key:zDn… | The person when signing a document | Derived from the holder key; the DID contains the public key | no | Yes |
| Status index + uri | The PID's position in the issuer's status list | Inside the PID; the list is published by the issuer | no | Yes, a random index that reveals nothing |
credentialId | The PID in the issuer's records | Inside the PID (demo only) | no | Yes, one per PID |
Demo simplifications: the signing DID uses the same key as the PID's holder binding; in a real wallet these are separate keys. The credentialId is visible to verifiers here; in practice it would not be, because it makes presentations linkable.
Issuing a PID
sequenceDiagram actor You participant Wallet as hub / wallet participant Issuer as issuer participant Registry as registry participant Ledger as Besu ledger Note over Issuer,Ledger: At startup (once per issuer key) Issuer->>Registry: POST /did/register (issuer DID, public key) Registry->>Ledger: tx registerDID Ledger-->>Registry: block + event DIDRegistered Note over You,Issuer: Every issuance - nothing is written to the ledger You->>Wallet: Start issuance (chosen person) Wallet->>Issuer: POST /credential (access token + proof of holder key) Issuer->>Issuer: Reserve random index in the status list Issuer-->>Wallet: PID bound to the holder key, with status idx + uri
Signing a document
The ledger plays no part here. In production a qualified signature also needs a qualified certificate from a trust service (QTSP) on the EU Trusted List.
sequenceDiagram actor You participant Hub as hub participant Wallet as wallet participant Verifier as verifier You->>Hub: Start signing (chosen person) Hub->>Wallet: POST /sign (document, person) Wallet-->>Hub: JWS over the document hash + did:key of that person Hub->>Verifier: POST /verify-signature Verifier->>Verifier: Derive public key from the did:key, check signature + hash Verifier-->>Hub: verified
Timestamping
sequenceDiagram actor You participant Wallet as hub / wallet participant TSA as TSA participant Verifier as verifier participant Registry as registry participant Ledger as Besu ledger You->>Wallet: Start timestamp Wallet->>TSA: POST /timestamp (SHA-256 hash only) TSA->>Registry: POST /timestamp/anchor (hash) Registry->>Ledger: tx anchorTimestamp Ledger-->>Registry: block (+ event TimestampAnchored, first time only) TSA-->>Wallet: timestamp token Wallet->>Verifier: POST /verify-timestamp Verifier->>Registry: GET /timestamp/:hash Registry->>Ledger: view timestampOf (free) Registry-->>Verifier: anchoring time
What is only read
| Who reads | When | Source |
|---|---|---|
| verifier | Every presentation: is the PID revoked? | status list at the issuer, not the ledger |
| verifier | Checking a timestamp | timestampOf |
| dashboard (via hub) | Every few seconds, for the statistics | events (eth_getLogs) |
| nobody | Not used | resolveDID resolveSignerKey isRevoked |
Wie schrijft wat op de ledger, en wanneer
Op de ledger staat één contract, EUidentityRegistry. Alleen de registry-service heeft een account dat transacties ondertekent. Issuer en TSA komen nooit zelf bij de ledger: ze vragen de registry via HTTP om iets vast te leggen. Alleen organisaties en hashes komen op de ledger, nooit personen.
Overzicht
flowchart LR U(["Jij: dashboard of telefoon"]) --> H[hub / wallet] H -->|PID ophalen| I[issuer] H -->|tijdstempel vragen| T[TSA] H -->|handtekening laten checken| V[verifier] I -->|"POST /did/register, eenmalig bij opstarten"| R[registry] T -->|POST /timestamp/anchor| R V -.->|"GET /timestamp/:hash"| R V -.->|"GET statuslijst"| I R ==>|"enige schrijver: transactie"| L[(Besu ledger<br/>EUidentityRegistry)] R -.->|gratis view-call| L
Dikke pijl: schrijven (transactie, blok, event). Stippellijn: lezen, laat geen spoor op de ledger.
Wat komt er op de ledger
| Wat | Wanneer | Aanvrager | Contractfunctie · event | Bijzonderheid |
|---|---|---|---|---|
| Issuer-DID + publieke sleutel | Eén keer, bij het opstarten van de issuer | issuer | registerDIDDIDRegistered | did:web:issuer.euidentity.nl. De issuer maakt bij elke start een nieuwe sleutel, dus een herstart is een sleutelrotatie. Een PID uitgeven schrijft niets. |
| Hash van een document (tijdstempel) | Bij tijdstempelen | TSA | anchorTimestampTimestampAnchored | Alleen de eerste keer telt. Dezelfde hash nog eens: wel een transactie, geen event. |
| Ondertekensleutel van een persoon | Nooit | niemand | registerSignerKey | Personen tekenen met een eigen did:key; de publieke sleutel zit in de DID. |
| Ingetrokken credential | Nooit | niemand | revokeCredential | Intrekking loopt via de statuslijst van de issuer. Zie Intrekken in het menu. |
Welke identifiers horen bij een PID
Een PID bevat meerdere verwijzingen. Alleen de DID van de issuer komt op de ledger. Alles wat bij de persoon hoort, blijft in de wallet of in de PID zelf.
flowchart LR
subgraph PID["PID van één persoon (SD-JWT VC of mdoc)"]
iss["iss<br/>https://issuer.euidentity.nl"]
cnf["cnf.jwk / deviceKey<br/>publieke holder-sleutel"]
st["status.status_list<br/>idx + uri"]
cid["credentialId<br/>pid-…"]
end
iss --> IDID["did:web:issuer.euidentity.nl<br/>issuer-DID, één per issuer"]
IDID ==> L[(Besu ledger)]
iss --> TL["EU Trusted List<br/>certificaat + sleutel issuer"]
cnf --> HK["holder-sleutel in de wallet<br/>één per persoon, verlaat de wallet nooit"]
HK --> DK["did:key:zDn…<br/>onderteken-DID, afgeleid van de sleutel<br/>nooit op een ledger"]
st --> SL["statuslijst bij de issuer<br/>bit idx: 0 = geldig, 1 = ingetrokken"]
cid -.-> IR["eigen administratie issuer<br/>alleen in de demo"]| Identifier | Identificeert | Waar het staat | Op de ledger | Per persoon |
|---|---|---|---|---|
did:web:issuer.euidentity.nl | De uitgever van de PID (iss) | Ledger en EU Trusted List | ja | Nee, één per issuer |
Holder-sleutel (cnf.jwk / deviceKey) | De wallet waaraan de PID gebonden is | In de PID; de privésleutel in het secure element van de wallet | nee | Ja |
did:key:zDn… | De persoon bij het ondertekenen van een document | Afgeleid van de holder-sleutel; de DID bevat de publieke sleutel | nee | Ja |
| Status-index + uri | De plek van de PID in de statuslijst van de issuer | In de PID; de lijst publiceert de issuer | nee | Ja, een willekeurige index die niets verraadt |
credentialId | De PID in de administratie van de issuer | In de PID (alleen in de demo) | nee | Ja, één per PID |
Vereenvoudigingen in de demo: de onderteken-DID gebruikt dezelfde sleutel als de holder-binding van de PID; in een echte wallet zijn dat aparte sleutels. De credentialId is hier zichtbaar voor verifiers; in de praktijk niet, omdat presentaties dan aan elkaar te koppelen zijn.
Een PID uitgeven
sequenceDiagram actor Jij participant Wallet as hub / wallet participant Issuer as issuer participant Registry as registry participant Ledger as Besu ledger Note over Issuer,Ledger: Bij het opstarten (eenmalig per issuer-sleutel) Issuer->>Registry: POST /did/register (issuer-DID, publieke sleutel) Registry->>Ledger: tx registerDID Ledger-->>Registry: blok + event DIDRegistered Note over Jij,Issuer: Bij elke uitgifte - er wordt niets op de ledger geschreven Jij->>Wallet: Start uitgifte (gekozen persoon) Wallet->>Issuer: POST /credential (access token + bewijs holder-sleutel) Issuer->>Issuer: Reserveer willekeurige index in de statuslijst Issuer-->>Wallet: PID gebonden aan de holder-sleutel, met status idx + uri
Een document ondertekenen
Hier komt de ledger niet aan te pas. In productie hoort bij een gekwalificeerde handtekening ook een gekwalificeerd certificaat van een vertrouwensdienst (QTSP) op de EU Trusted List.
sequenceDiagram actor Jij participant Hub as hub participant Wallet as wallet participant Verifier as verifier Jij->>Hub: Start ondertekenen (gekozen persoon) Hub->>Wallet: POST /sign (document, persoon) Wallet-->>Hub: JWS over de document-hash + did:key van die persoon Hub->>Verifier: POST /verify-signature Verifier->>Verifier: Publieke sleutel afleiden uit de did:key, handtekening + hash checken Verifier-->>Hub: geverifieerd
Tijdstempelen
sequenceDiagram actor Jij participant Wallet as hub / wallet participant TSA as TSA participant Verifier as verifier participant Registry as registry participant Ledger as Besu ledger Jij->>Wallet: Start tijdstempel Wallet->>TSA: POST /timestamp (alleen de SHA-256-hash) TSA->>Registry: POST /timestamp/anchor (hash) Registry->>Ledger: tx anchorTimestamp Ledger-->>Registry: blok (+ event TimestampAnchored, alleen de eerste keer) TSA-->>Wallet: tijdstempeltoken Wallet->>Verifier: POST /verify-timestamp Verifier->>Registry: GET /timestamp/:hash Registry->>Ledger: view timestampOf (gratis) Registry-->>Verifier: tijdstip van verankering
Wat alleen gelezen wordt
| Wie leest | Wanneer | Bron |
|---|---|---|
| verifier | Bij elke presentatie: is de PID ingetrokken? | statuslijst bij de issuer, niet de ledger |
| verifier | Tijdstempel controleren | timestampOf |
| dashboard (via hub) | Elke paar seconden, voor de statistieken | events (eth_getLogs) |
| niemand | Niet gebruikt | resolveDID resolveSignerKey isRevoked |