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 www.aloaha.com erreichbar; die Antwortformate sind gegen den laufenden Build verifiziert. Wallets und Integratoren nutzen diese Seite als kanonische Integrationsreferenz.
Von Aloaha Limited gebaut. Kein Fork eines SDKs. Jeder unten dokumentierte Endpunkt ist intern implementiert — SD-JWT-VC-Parsing, JWS- + KB-JWT-Verifikation, OpenID4VP Request/Response, DCQL, JWE ECDH-ES, x5c-Chain-Validierung, StatusList-Revokation, OpenID Federation 1.0. Kein eudi-lib-*, kein Drittanbieter-EUDI-SDK im Dependency-Tree. Drei konkrete Zusagen: (1) der Code gehört uns — jede EUDI-Protokoll-Zeile prüfbar, keine geschlossenen EUDI-Bibliotheken, denen Ihr CISO blind vertrauen müsste (generische Infrastruktur-Libs sind Open-Source-Standardkomponenten); (2) eigener Release-Rhythmus — CVE-Patches und Features landen auf unserem Zeitplan, nicht abhängig von einer SDK-Version eines Konsortiums; (3) strikt standardbasierte Interoperabilität — Wallet, Verifier und Issuer sprechen ausschließlich über veröffentlichte RFCs und EUDI-Protokolle, jedes Stück austauschbar, kein Vendor-Lock-in auch zwischen unseren Komponenten.
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:www.aloaha.com (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.aloaha.com/oidc.ashx?action=vp-start&client_id_prefix=x509_hash'
# Kompatibilitätsprofil (x509_san_dns gemäß HAIP 1.0 Final).
curl 'https://phone.aloaha.com/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.aloaha.com/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": "www.aloaha.com" // 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.aloaha.com/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.aloaha.com/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.aloaha.com/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).