OID4VP-1.0-Verifier (Relying Party) für das EU Digital Identity Wallet und jede Wallet, die denselben Dialekt spricht. Jeder unten dokumentierte Endpunkt ist auf dem Live-Host phone.codeb.io erreichbar; die Antwortformate sind gegen den laufenden Build verifiziert. Wallets und Integratoren nutzen diese Seite als kanonische Integrationsreferenz.
Mit x509_hash arbeiten — dem Wallet‑bevorzugten Client‑Identifier‑Prefix.x509_hash (OID4VP 1.0 §5.9.3, base64url SHA‑256 des DER‑Leaf‑Zertifikats) ist die Form, an die sich reale EUDI‑Wallets primär binden. Der Verifier unterstützt zusätzlich x509_san_dns (HAIP 1.0 Final) als Kompatibilitätsoption. Der Aufrufer von vp‑start wählt das Prefix pro Session via ?client_id_prefix=x509_hash|x509_san_dns; Wallet-Integratoren sollten ausdrücklich x509_hash anfragen. Server-Standard ohne Parameter ist nun x509_hash — passend zum bevorzugten Wallet-Pfad. Einmal gewählt, ist das Prefix an die Session gebunden und wird persistiert, damit der client_id-Claim im JAR und die ExpectedClientId-Validierung konsistent bleiben.
Verifier-Deskriptor (Relying Party) — listet kryptographische Algorithmen, Response‑Encryption-Modi, unterstützte Credential-Formate und die Endpunkte, mit denen eine Wallet sprechen soll. Folgt dem von EUDI-Referenz-Deployments etablierten Verifier-Metadata-Muster.
x509_hash_value — base64url SHA‑256 des DER‑Leaf‑Zertifikats. Das ist der primäre Identifier, an den reale EUDI‑Wallets binden; x509_hash:<hash> ist die empfohlene Client‑Identifier‑Prefix‑Form.
client_id_schemes_supported — vollständige Liste der pro‑Request akzeptierten Prefixes (x509_hash empfohlen; x509_san_dns für HAIP 1.0 Final-Kompatibilität).
client_id — der prefixierte Identifier, wenn ?client_id_prefix= nicht an vp‑start übergeben wird. Server-Standard ist nun x509_hash (Wallet‑bevorzugte Form).
vp_formats_supported — CodeB liefert ausschließlich dc+sd-jwt. mdoc / ISO mDL ist nicht im Scope.
authorization_encrypted_response_alg / _enc — Wallets MÜSSEN die VP-Response verschlüsseln (JWE), wenn der Request direct_post.jwt wählt.
Standard-OpenID-Connect-Discovery-Dokument. Listet OIDC-Endpunkte, Claim-Namen, Signaturalgorithmen sowie die amr/acr-Werte, die der Verifier für Wallet-basierte Sessions ausgibt. Selber Handler wie der restliche OIDC-IdP — vollständige Hülle siehe oidc_api.html; die EU-Wallet-relevanten Teile unten.
EU-Wallet-relevante Felder
acr_values_supported enthält eudi:pid:high, eudi:pid:substantial, urn:codeb:vc:member — der Verifier markiert die resultierende OIDC-Session mit acr=urn:codeb:acr:eudi-wallet, wenn die Authentifizierung über den EU-Wallet-Flow erfolgte.
amr_values_supported enthält implizit vc — wird in der SSO-Assertion gemeldet (siehe §SSO-Assertion).
claims_supported deckt die Standard-PID-Claim-Form ab, die der EU-PID-Issuer liefert (given_name, family_name, birth_date, email_address, etc.).
Startet einen Verifiable-Presentation-Flow. Aufrufer ist üblicherweise eine CodeB‑gehostete Login-Seite (logineu.html) oder ein externer Integrator. Zurückgegeben werden die Session-ID, die URL, über die die Wallet den signierten Request abholt, und ein Wallet‑Invocation-Deep-Link.
Request
Kein Body. Query-Parameter:
client_id_prefix — optional, einer von x509_hash (empfohlen für reale EUDI‑Wallets gemäß OID4VP 1.0 §5.9.3) oder x509_san_dns (HAIP 1.0 Final-Kompatibilitätsprofil). Bindet das Client-Identifier-Prefix-Schema an die Session. Server-Standard ohne Parameter: x509_hash.
Das gewählte Prefix steckt im client_id-Wert: x509_hash:<base64url‑sha256‑des‑DER‑Leafs> (Wallet‑bevorzugt) bzw. x509_san_dns:phone.codeb.io (HAIP 1.0 Final). Aufrufer können am ersten : trennen, um das Prefix zu extrahieren. status startet bei pending und wird beim Wallet-Roundtrip fortgeschrieben.
Beispiele
# Wallet‑bevorzugt (x509_hash gemäß OID4VP 1.0 §5.9.3) — empfohlen für reale EUDI‑Wallets.
curl 'https://phone.codeb.io/oidc.ashx?action=vp-start&client_id_prefix=x509_hash'
# Kompatibilitätsprofil (x509_san_dns gemäß HAIP 1.0 Final).
curl 'https://phone.codeb.io/oidc.ashx?action=vp-start&client_id_prefix=x509_san_dns'
# Ohne Parameter: Server fällt jetzt auf x509_hash zurück (Wallet‑bevorzugt).
curl https://phone.codeb.io/oidc.ashx?action=vp-start
Session-Lebenszyklus
Die Session-ID ist opak, ca. 16 hexadezimale Zeichen, tenant-skopiert. Der Session-Datensatz wird auf Platte unter App_Data/<tenant>/vp‑sessions/<id>.json persistiert, damit App-Pool-Recycles und Multi-Worker-IIS-Deployments den Zustand nicht verlieren. Sessions laufen nach expires_in Sekunden ab; abgebrochene Sessions zählen in die vp_abandoned-Metrik von ping.
Die Wallet ruft diesen Endpunkt auf, um den signierten Authorization-Request (JAR — JWT‑Secured Authorization Request, RFC 9101) zu erhalten. Der Body ist ein einzelnes ES256‑signiertes JWT mit der x5c-Zertifikatskette des Verifier-Leafs im JWS-Header.
Die Wallet sendet die Verifiable Presentation hier hoch. Der Verifier akzeptiert beide in OID4VP 1.0 §8.4 definierten Wire-Formen — wähle, was die Wallet liefert; der Handler erkennt die Form automatisch.
Das <JWE> ist mit ECDH‑ES-Schlüsselvereinbarung und A128GCM-Content-Encryption verschlüsselt, gegen den ephemeren JWK, den der Verifier im JAR-Feld client_metadata.jwks mitgegeben hat. Der Klartext im JWE ist der form‑kodierte Body aus Form B.
Das vp_token ist die SD‑JWT VC mit selektiv offengelegten Claims und angehängtem KB‑JWT für das Holder-Binding. Format: <Issuer‑signiertes JWT>~<Disclosure1>~<Disclosure2>~...~<KB‑JWT>.
redirect — wohin die aufrufende Seite den Browser als Nächstes leiten soll. Auf dem Standard-Mandanten immer /account.html; kann eine tiefere Return-URL sein, wenn der Wallet-Flow von dort gestartet wurde.
sso_assertion — RS256-signiertes Assertion-JWT, das der Browser-Tab im sessionStorage ablegt und am /oauth2/v1/authorize gegen einen OIDC-Code eintauscht (siehe SSO-Assertion).
sso_max_age — Assertion-Lebensdauer in Sekunden.
vct + issuer — Credential-Typ und Issuer der gerade verifizierten SD-JWT VC. Die PID-Claims selbst werden in den Benutzerdatensatz auf der Platte geschrieben; sie werden nicht im Response-Body wiederholt. Nach dem Assertion-Tausch sind die Claims über /oauth2/v1/userinfo abrufbar.
disclosures_verified — Anzahl selektiv offengelegter Claims, deren Hash gegen den SD-Claim-Digest im Issuer-signierten JWT passte.
Fehler-Response
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"ok": false,
"error": "invalid_vp_token",
"error_description": "kb-jwt signature did not verify against holder cnf"
}
Jede Stufe emittiert ein strukturiertes Log-Event mit Session-ID (vp‑shape‑detected, vp‑verify‑result, vp‑user‑resolved, vp‑sso‑minted, etc.). Der Pfad einer fehlschlagenden Wallet lässt sich allein aus dem OIDC-Log nachvollziehen.
Die Erfolgs-Response von vp‑response enthält sso_assertion — ein kurzlebiges RS256-signiertes JWT, das jede CodeB-föderierte Applikation gegen eine regäläre OIDC-Session eintauschen kann. So entsteht aus der Wallet-Präsentation eine normale Anmeldung.
Führe einen Standard-OIDC-Authorization-Code-Flow mit PKCE gegen /oauth2/v1/authorize wie in oidc_api.html beschrieben. Füge acr_values=urn:codeb:acr:eudi-wallet hinzu, um eine EU-Wallet-basierte Anmeldung zu erzwingen.
Der CodeB-IdP leitet den Nutzer durch logineu.html, die im Hintergrund vp‑start, vp‑request und vp‑response ansteuert.
Bei Erfolg schließt der IdP den OIDC-Flow normal ab — deine Applikation erhält einen Authorization Code, tauscht ihn am /oauth2/v1/token-Endpunkt ein und bekommt id_token und access_token, als hätte sich der Nutzer per Passwort angemeldet.
Das id_token trägt amr: ["vc"] und acr: "urn:codeb:acr:eudi-wallet", dazu die PID-Claims, die deine App über Standard-OIDC-Scopes (profile, email) angefordert hat.
Aus dem Wallet-Flow geminte User-IDs sind subjektstabil (z. B. pin_de_3f8a1d7e für eine PID mit Personalverwaltungsnummer, eu_3f8a1d7e für den Name-und-Geburtsdatum-Hash als Fallback). Beim ersten Sign-In ist die Rolle guest; Admins können die Rolle erhöhen.
Tausche eine SSO-Assertion (die vp‑response-Erfolgsantwort) gegen ein OIDC-access_token + id_token + refresh_token. Praktisch für Service-zu-Service-Flows und Integratoren (oder Test-Skripte), die nach erfolgreicher Wallet-Präsentation ein Bearer brauchen, ohne den Browser durch /authorize zu schleifen.
Request
POST /oauth2/v1/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<sso_assertion> (aus der vp-response-Erfolgsantwort)
&client_id=<client_id> (registrierter Client)
&client_secret=<secret> (nur Confidential-Clients)
&scope=openid (optional, Standard openid)
Type-Guard. Der typ-Claim der Assertion muss "sso" sein. access_token oder id_token im assertion-Parameter werden mit invalid_grant abgelehnt — Schutz gegen Token-Reuse (Confused-Deputy).
Lese den Konto-Zustand des angemeldeten Nutzers. Unmittelbar vor account=password aufgerufen, damit der Aufrufer den Realm (für die HA1-Berechnung) und das has_password-Flag kennt.
Request
GET /signal.ashx?account=get
Authorization: Bearer <access_token>
Erfolgsantwort (200)
{
"user": "eu_3f8a1d7e",
"role": "guest",
"disabled": false,
"profile": { ... gespeicherte OIDC-Claims ... },
"has_password": false, // true wenn bereits HA1 gespeichert
"realm": "phone.codeb.io" // für HA1 = MD5(user:realm:password)
}
Fehler
401 invalid_token — Bearer fehlt, abgelaufen oder falsche Audience.
Warum Realm zurückgeben? Die Credentials-Datei nutzt SIP-Digest-HA1 = MD5(user:realm:password). Wenn account=get den Realm liefert, kann der Aufrufer HA1 client-seitig berechnen ohne separaten Config-Lookup; das Klartext-Passwort verlässt den Browser nie.
Setze oder rotiere das Passwort des angemeldeten Nutzers. Drei Pfade, abhängig vom gespeicherten Zustand und dem Bearer-Faktor:
Erstvergabe (Nutzer hat noch kein HA1) — current_ha1 NICHT erforderlich. Wallet-erstellte Gäste aktivieren so Benutzername-+-Passwort-Anmeldung.
Wallet‑als‑Recovery (Nutzer hat HA1, Bearer trägt acr=urn:codeb:acr:eudi-wallet) — current_ha1 NICHT erforderlich. Die Wallet-Authentifizierung ist der Identitätsnachweis; klassischer "Passwort vergessen"-Pfad.
Reguläre Änderung (Nutzer hat HA1, Bearer ist passwort‑authentifiziert) — current_ha1 MUSS mitgesendet werden und wird constant-time verglichen.
Request
POST /signal.ashx?account=password
Authorization: Bearer <access_token>
Content-Type: application/json
{
"ha1": "<32 Hex-Zeichen kleingeschrieben = MD5(user:realm:password)>",
"current_ha1": "<optional; nur bei regulärer Änderung Pflicht>"
}
Beide Flags können bei einer ganz neuen Wallet-gesteuerten Erstvergabe true sein — der Nutzer hatte noch kein Passwort (first_set) UND die Wallet-Auth war der Nachweis (wallet_recovery).
Fehler
400 missing_ha1 / invalid_ha1 — Body fehlt oder fehlerhaft.
400 missing_current_ha1 — current_ha1 wird für diese Änderung gebraucht, wurde aber nicht mitgesendet.
400 invalid_current_ha1 — Wert hat nicht die richtige HA1-Form.
400 new_equals_current — neues Passwort ist identisch zum aktuellen.
401 current_password_incorrect — current_ha1 passt nicht zum gespeicherten Wert.
Audit-Spur
Drei verschiedene Ereignisnamen im Connection-Log, damit Operatoren die Pfade unterscheiden können:
Die gesamte Integrationsstory. Sechs Request/Response-Hops, mit denen ein brandneuer EUDI-Wallet-Nutzer (a) sich per Wallet anmeldet, (b) im selben wallet-authentifizierten Session-Kontext Benutzername-+-Passwort-Anmeldung aktiviert und (c) per Anmeldung die Funktionalität verifiziert. Identische Form wie eu‑wallet‑mock.py --set-password — das ausgelieferte automatisierte Test-Skript für genau diesen Flow.
Vorbedingung: Integrator hat bereits vp‑start → vp‑request → vp‑response durchlaufen und hält:
sso_assertion aus dem Erfolgs-Body von POST /oidc.ashx?action=vp‑response (Single-Use, 30 Min TTL).
Den Benutzernamen aus demselben Body (z. B. eu_3f8a1d7e).
Schritt 1 — SSO-Assertion gegen OIDC-access_token tauschen
Schritt 6 — (Manuelle UI-Anmeldung) der Nutzer ruft /login.html auf und meldet sich an
Benutzername = eu_3f8a1d7e (oder das, was vp‑response zurückgab), Passwort = der Klartext aus Schritt 3. Der Browser hashed lokal zu HA1; der Server vergleicht mit dem Wert aus Schritt 4. Der Nutzer ist nun dauerhaft sowohl per Wallet als auch per Passwort anmeldebar.
Forgot-Password-Pfad
Exakt dieselben sechs Schritte funktionieren, wenn der Nutzer bereits ein Passwort hat. Schritt 4 liefert first_set: false, wallet_recovery: true — die Wallet-Auth ersetzt current_ha1. Schritt 5 bestätigt, dass das neue Passwort funktioniert. Im Audit-Log steht account-password-wallet-reset.
Sicherheits-Argument. Das Überspringen von current_ha1 geschieht nur, wenn der Bearer acr=urn:codeb:acr:eudi-wallet trägt — dieser Wert wird ausschließlich gesetzt, wenn der OID4VP-Verifier einen vollständigen SD‑JWT-VC- + KB‑JWT-Roundtrip mit kryptographisch verifizierter Holder-Bindung abgeschlossen hat. Eine erfolgreiche Wallet-Auth ist mindestens so stark wie ein Passwort; zusätzlich current_ha1 zu verlangen wäre ein UX-Bug, kein Sicherheits-Feature.
TRYeu-wallet-mock-both.py · lokale Mock-Wallet-Komplettrunde #
Der mitgelieferte Test-Harness durchläuft beide Client-Identifier-Prefixes hintereinander, end-to-end. Liefern beide Aufrufe HTTP 200 mit offengelegten PID-Claims und einer SSO-Assertion, funktioniert die Integration.
Ablauf
# 1. Session mit x509_hash starten (Wallet‑bevorzugt)
GET https://phone.codeb.io/oidc.ashx?action=vp-start&client_id_prefix=x509_hash
→ { id, request_uri, deep_link, client_id, status }
# 2. Signierten Request abholen (wie es die Wallet tun würde)
GET {request_uri}
→ ES256-signiertes JWT, application/oauth-authz-req+jwt
# 3. JWS verifizieren, VP-Response bauen
# (SD-JWT VC + KB-JWT gegen den ephemeren ECDH-ES-Schlüssel des Verifiers)
# 4. Response posten
POST https://phone.codeb.io/oidc.ashx?action=vp-response&id={id}
Content-Type: application/x-www-form-urlencoded
Body: response=<JWE>
→ { ok:true, vct, iss, disclosures_verified, claims, sso_assertion }
# 5. Mit client_id_prefix=x509_san_dns wiederholen, um den HAIP‑1.0‑Final
# Kompatibilitätspfad zu verifizieren. Selbes Skript, nur Query-Parameter umstellen.
Build-Abgleich
# Bestätige, dass du den erwarteten Build testest.
curl https://phone.codeb.io/oidc.ashx?action=ping
# build = aktuelle Handler-Version. Wird bei jeder relevanten Änderung erhöht.
Log-Events für Operator-Greps
Jede in vp‑response aufgeführte Stufe emittiert ein strukturiertes Event mit Session-ID; dazu kommen einige Fehlerpfad-Events. Greppbares Vokabular (Ausschnitt):
Einschränkungen & defensiver Geltungsbereich. Der CodeB-Verifier ist Relying Party, weder eine notifizierte nationale Wallet noch ein qualifizierter Vertrauensdiensteanbieter. Die Protokoll-Substanz ist live und verifizierbar (Proof of Work); High-Assurance-Identitätsprüfung wartet auf iter‑2-Härtung (Issuer-Signaturkette gegen die EU-Liste vertrauenswürdiger Listen, OAuth-Status-List-Widerruf, Wallet-Attestation-Konformität).