EU-Wallet · Verifier-API

EU-Wallet-Verifier · öffentliche API

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.

GET /oidc.ashx?action=ping #

Health-Check und Live-Verifier-Metriken. Öffentlich, ohne Authentifizierung. Für Monitoring oder zur Bestätigung einer deployten Build-Version.

Response

JSON-Body. Felder im Detail:

  • oktrue, wenn der OIDC-Handler initialisiert ist.
  • build — Build-Version des Handlers. Format YYYY-MM-DD-slug. Wird bei jeder relevanten Änderung erhöht.
  • tenant — aufgelöster Tenant (Multi-Tenancy nach Domain).
  • now — aktuelle Serverzeit in Sekunden seit der Epoche.
  • vp_started — Zähler aller seit Service-Start angelegten vp‑start-Sessions.
  • vp_completed — Zähler aller vp‑response-Verifikationen, die mit ok=true abgeschlossen wurden.
  • vp_abandoned — Zähler abgebrochener Sessions (gestartet, keine Antwort innerhalb der TTL). Damit lässt sich die Abbruchrate ablesen.
  • vp_pending_or_inflight — Gauge der momentan auf eine Wallet-Antwort wartenden Sessions.

Beispiel

curl https://phone.codeb.io/oidc.ashx?action=ping
{
  "ok": true,
  "build": "2026-06-09-vp-traceall",
  "tenant": "phone.codeb.io",
  "now": 1780995196,
  "vp_started": 2,
  "vp_completed": 2,
  "vp_abandoned": 0,
  "vp_pending_or_inflight": 0
}

GET /oidc.ashx?action=verifier-metadata #

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.

Response (Live-Form)

{
  "client_id": "x509_san_dns:phone.codeb.io",
  "client_id_scheme": "x509_san_dns",
  "client_id_schemes_supported": ["x509_san_dns","x509_hash"],
  "x509_hash_value": "4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "request_object_signing_alg_values_supported": ["ES256"],
  "vp_formats_supported": {
    "dc+sd-jwt": {
      "sd-jwt_alg_values": ["ES256"],
      "kb-jwt_alg_values": ["ES256"]
    }
  },
  "authorization_encrypted_response_alg": ["ECDH-ES"],
  "authorization_encrypted_response_enc": ["A128GCM"],
  "x509_thumbprint_sha1": "EC9DE3346BB69BBB52D0023BDF47D0E041A97A9D",
  "request_uri_endpoint":  "https://phone.codeb.io/oidc.ashx?action=vp-request&id={id}",
  "response_uri_endpoint": "https://phone.codeb.io/oidc.ashx?action=vp-response&id={id}",
  "vp_start_endpoint":     "https://phone.codeb.io/oidc.ashx?action=vp-start",
  "status": "live",
  "notes": "OID4VP 1.0 end-to-end live. Beide Client-Identifier-Prefix-Schemata unterstützt."
}

Feldhinweise

  • 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.

GET /.well-known/openid-configuration #

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.).

Auch erreichbar als /oidc.ashx?action=discovery.

GET /oidc.ashx?action=vp-start #

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.

Response

{
  "id": "vp-7c5f0e3d4b1a4f9c",
  "request_uri": "https://phone.codeb.io/oidc.ashx?action=vp-request&id=vp-7c5f0e3d4b1a4f9c",
  "deep_link": "openid4vp://?client_id=x509_hash%3A4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw&request_uri=https%3A%2F%2Fphone.codeb.io%2Foidc.ashx%3Faction%3Dvp-request%26id%3Dvp-7c5f0e3d4b1a4f9c",
  "client_id": "x509_hash:4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "expires_in": 300,
  "status": "pending"
}

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.

GET /oidc.ashx?action=vp-request&id={id} #

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.

Response-Header

Content-Type: application/oauth-authz-req+jwt
Cache-Control: no-store

Response-Body

ES256-signiertes JWT. Dekodierter Payload (exemplarisch):

{
  "iss":  "x509_hash:4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "aud":  "https://self-issued.me/v2",
  "iat":  1780995100,
  "exp":  1780995400,
  "nonce": "fA72c1...zufälliges base64url",
  "client_id":         "x509_hash:4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "client_id_scheme":  "x509_hash",
  "response_type":     "vp_token",
  "response_mode":     "direct_post.jwt",
  "response_uri":      "https://phone.codeb.io/oidc.ashx?action=vp-response&id=vp-7c5f0e3d4b1a4f9c",
  "client_metadata": {
    "authorization_encrypted_response_alg": "ECDH-ES",
    "authorization_encrypted_response_enc": "A128GCM",
    "jwks": { "keys": [ /* ephemerer ECDH-ES-Schlüssel für die Response-Verschlüsselung */ ] }
  },
  "dcql_query": {
    "credentials": [
      {
        "id": "pid",
        "format": "dc+sd-jwt",
        "meta": { "vct_values": ["urn:eu.europa.ec.eudi:pid:1"] },
        "claims": [
          { "path": ["given_name"] },
          { "path": ["family_name"] },
          { "path": ["birth_date"] },
          { "path": ["email_address"] }
        ]
      }
    ]
  }
}

JWS-Header (exemplarisch)

{
  "alg": "ES256",
  "typ": "oauth-authz-req+jwt",
  "x5c": [
    "MIIB...Verifier-Leaf-Zertifikat, base64-kodierte DER",
    "MIIB...Intermediate (falls vorhanden)"
  ]
}

Hinweise

  • Der client_id-Claim ist mit dem vom Aufrufer bei vp‑start gewählten Prefix versehen (x509_san_dns: oder x509_hash:).
  • Bei x509_hash ist der Prefix-Wert base64url(SHA‑256(DER Leaf)), identisch mit dem x509_hash_value in verifier‑metadata.
  • Die Wallet validiert die Signatur gegen das Leaf in der x5c-Kette und gleicht das gewählte Prefix mit dem client_id-Claim ab.

POST /oidc.ashx?action=vp-response&id={id} #

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.

Form A — direct_post.jwt (verschlüsselt)

Content-Type: application/x-www-form-urlencoded

response=<JWE>

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.

Form A — Variante

Content-Type: application/oauth-authz-resp+jwt

<JWE>

Manche Wallets liefern das JWE als rohen Body statt form-verpackt. Der Verifier akzeptiert beides.

Form B — direct_post (Klartext, form-kodiert)

Content-Type: application/x-www-form-urlencoded

vp_token=<SD-JWT-VC>&state=<optional>

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

Erfolgs-Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "ok": true,
  "redirect": "/account.html",
  "sso_assertion": "eyJhbGciOiJSUzI1NiIs...",
  "sso_max_age": 3600,
  "vct": "urn:eu.europa.ec.eudi:pid:1",
  "issuer": "https://issuer.eudiw.dev/pid",
  "disclosures_verified": 4
}

Feldhinweise

  • 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"
}

Verifikationsstufen (in Reihenfolge)

  1. Body-Form-Erkennung (JWE‑form‑unwrap / JWE‑raw / plain).
  2. JWE-Decrypt (ECDH‑ES + A128GCM), falls anwendbar.
  3. SD‑JWT-VC-Envelope-Parse + Struktur-Validierung.
  4. Disclosure-Hash-Prüfung gegen die SD-Claim-Digests.
  5. KB‑JWT-Signaturprüfung gegen den Holder-Binding-Schlüssel in cnf.
  6. User-Identifier-Bindung (eine von: userHint, personal_administrative_number, email_address, pid_name_dob_hash, anonymous).
  7. SSO-Assertion-Mint (RS256, OIDC-kompatibel).

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.

SSO sso_assertion · OIDC-Code-Flow-Übergabe #

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.

Assertion-Payload (exemplarisch)

{
  "iss":  "https://phone.codeb.io",
  "sub":  "eu_3f8a1d7e",
  "role": "guest",
  "amr":  ["vc"],
  "acr":  "urn:codeb:acr:eudi-wallet",
  "iat":  1780995200,
  "exp":  1780998800
}

Konsumption auf der Relying-Party-Seite

  1. 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.
  2. Der CodeB-IdP leitet den Nutzer durch logineu.html, die im Hintergrund vp‑start, vp‑request und vp‑response ansteuert.
  3. 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.
  4. 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.

POST /oauth2/v1/token · JWT‑Bearer-Grant (RFC 7523) #

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)

Erfolgsantwort (200)

{
  "access_token":  "eyJhbGciOiJSUzI1NiIs...",
  "token_type":    "Bearer",
  "expires_in":    3600,
  "id_token":      "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "...",
  "scope":         "openid"
}

Fehler

  • 400 invalid_requestassertion oder client_id fehlt.
  • 401 invalid_grant — Assertion fehlerhaft, falsche aud, falscher typ (muss sso sein) oder absolute auth_time-Grenze (4 Stunden) überschritten.
  • 401 invalid_client — Confidential-Client hat client_secret nicht bestätigt.

Beispiel

curl https://phone.codeb.io/oauth2/v1/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  --data-urlencode "assertion=$SSO_ASSERTION" \
  -d "client_id=codeb-admin" \
  -d "scope=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).

GET /signal.ashx?account=get #

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.

POST /signal.ashx?account=password · Erstvergabe + Wallet‑als‑Recovery #

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>"
}

Erfolgsantwort (200)

{
  "ok":              true,
  "first_set":       true,    // true falls vorher kein Passwort vorhanden
  "wallet_recovery": true     // true falls Wallet-Auth current_ha1 ersetzt
}

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_ha1current_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_incorrectcurrent_ha1 passt nicht zum gespeicherten Wert.

Audit-Spur

Drei verschiedene Ereignisnamen im Connection-Log, damit Operatoren die Pfade unterscheiden können:

  • account-password-first-set — Wallet-Gast vergab erstes Passwort.
  • account-password-wallet-reset — bestehender Nutzer hat über Wallet zurückgesetzt.
  • account-password-changed — bestehender Nutzer hat regulär rotiert.

FLOW End‑to‑end-Walkthrough: Wallet → Passwort setzen → manuell anmelden #

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

curl https://phone.codeb.io/oauth2/v1/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  --data-urlencode "assertion=$SSO_ASSERTION" \
  -d "client_id=codeb-admin" \
  -d "scope=openid"

# -> { access_token, id_token, refresh_token, expires_in, scope }
ACCESS_TOKEN=$(... aus Antwort extrahieren ...)

Schritt 2 — user + realm + has_password lesen

curl https://phone.codeb.io/signal.ashx?account=get \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# -> { user: "eu_3f8a1d7e", role: "guest", has_password: false,
#      realm: "phone.codeb.io", ... }
USER="eu_3f8a1d7e"
REALM="phone.codeb.io"

Schritt 3 — HA1 = MD5(user:realm:password) berechnen

# Bash / OpenSSL
HA1=$(printf "%s:%s:%s" "$USER" "$REALM" "MeinNeuesPW" | md5sum | awk '{print $1}')

# Python
import hashlib
ha1 = hashlib.md5(f"{user}:{realm}:MeinNeuesPW".encode()).hexdigest()

Schritt 4 — Das neue HA1 an account=password senden

curl https://phone.codeb.io/signal.ashx?account=password \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"ha1\":\"$HA1\"}"

# -> { ok: true, first_set: true, wallet_recovery: true }
# (beide Flags true bei der allerersten Wallet-gesteuerten Vergabe)

Schritt 5 — Verifizieren: mit neuem Passwort anmelden

curl https://phone.codeb.io/oidc.ashx?action=login \
  -d "username=$USER" \
  -d "ha1=$HA1" \
  -d "return=/"

# -> { ok: true, user, role, sso_assertion, sso_max_age, redirect }
# Frische sso_assertion = Passwort funktioniert tatsächlich.

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.

TRY eu-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):

vp-start · vp-request · vp-response-arrived
vp-shape-detected · vp-verify-result
vp-user-resolved · vp-sso-minted
vp-response-ok · vp-response-rejected
vp-abandoned · vp-user-binding-invalid
vp-request-miss · vp-request-key-fail · vp-request-sign-fail
vp-response-miss · vp-response-body-read-fail
vp-response-input-build-fail · vp-response-lib-missing
vp-response-input-build-fail · vp-response-lib-missing
vp-response-process-throw · vp-response-write-fail
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).

Verwandte Dokumente. EU-Wallet-Proof of Work · /oidc.ashx (OIDC-IdP) API · Feature-Überblick · Live-Flow ausprobieren · API-Hub

Veröffentlicht 2026‑06‑09 · CodeB Sovereign Communications, gebaut von Aloaha Limited.