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 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, 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 (OpenID4VP-Kompatibilitätsmodus; nicht 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.aloaha.com/oidc.ashx?action=ping
{
  "ok": true,
  "build": "2026-06-09-vp-traceall",
  "tenant": "www.aloaha.com",
  "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_hash:4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw",
  "client_id_scheme": "x509_hash",
  "client_id_schemes_supported": ["x509_hash","x509_san_dns"],
  "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"]
    }
  },
  "encrypted_response_enc_values_supported": ["A128GCM","A256GCM"],
  "encrypted_response_alg_values_supported": ["ECDH-ES"],
  "x509_thumbprint_sha1": "EC9DE3346BB69BBB52D0023BDF47D0E041A97A9D",
  "request_uri_endpoint":  "https://phone.aloaha.com/oidc.ashx?action=vp-request&id={id}",
  "response_uri_endpoint": "https://phone.aloaha.com/oidc.ashx?action=vp-response&id={id}",
  "vp_start_endpoint":     "https://phone.aloaha.com/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 OpenID4VP-Kompatibilität mit älteren Wallets (nicht HAIP Final)).
  • 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.
  • encrypted_response_enc_values_supported — nach HAIP 1.0 Final; deklariert die Content-Encryption-Algorithmen, die dieser Verifier für direct_post.jwt-JWE-Responses akzeptiert. Sowohl A128GCM (Basis) als auch A256GCM werden unterstützt. Schlüsselvereinbarung ist ECDH-ES gegen den ephemeren JWK aus dem JAR-Feld client_metadata.jwks.

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 (OpenID4VP-Kompatibilitätsprofil für ältere Wallets; nicht HAIP Final). Bindet das Client-Identifier-Prefix-Schema an die Session. Server-Standard ohne Parameter: x509_hash.

Response

{
  "id": "vp-7c5f0e3d4b1a4f9c",
  "request_uri": "https://phone.aloaha.com/oidc.ashx?action=vp-request&id=vp-7c5f0e3d4b1a4f9c",
  "deep_link": "openid4vp://?client_id=x509_hash%3A4Ns4_AZgRtUzCbECSdEGgIdz_EG0iVqO8INWyufppZw&request_uri=https%3A%2F%2Fwww.aloaha.com%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:www.aloaha.com (OpenID4VP-Kompatibilitätsmodus; nicht HAIP 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'

# OpenID4VP-Kompatibilitätsprofil (x509_san_dns; NICHT HAIP Final -- HAIP schreibt x509_hash vor).
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.

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.aloaha.com/oidc.ashx?action=vp-response&id=vp-7c5f0e3d4b1a4f9c",
  "client_metadata": {
    "encrypted_response_enc_values_supported": ["A128GCM","A256GCM"],
  "encrypted_response_alg_values_supported": ["ECDH-ES"],
    "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"] }
        ],
        "trusted_authorities": [
          {
            "type": "aki",
            "values": [
              "hgn5vSlV_1Es8w1KE7TE0MxlBPk",
              "0FaWtvpnfNSJx7EEYqrPYlaJUnE"
            ]
          }
        ]
      }
    ]
  }
}

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_hash: oder x509_san_dns:).
  • 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.

DCQL trusted_authorities

Jede DCQL-Credential-Abfrage des Verifiers führt ein trusted_authorities-Array mit, damit die Wallet weiß, welchen Credential-Issuer-Autoritäten der Verifier für diese Transaktion vertraut. Struktur gemäß OpenID4VP §6 und HAIP 1.0 Final §6 (European Digital Identity Wallet-Profil):

"trusted_authorities": [
  {
    "type": "aki",
    "values": [
      "hgn5vSlV_1Es8w1KE7TE0MxlBPk",
      "0FaWtvpnfNSJx7EEYqrPYlaJUnE"
    ]
  }
]
  • type: "aki" bezeichnet Authority Key Identifier. Jeder Wert ist base64url(SubjectKeyIdentifier-Bytes) aus der X.509-SKI-Extension (OID 2.5.29.14) des Vertrauensanker-Zertifikats. Anker ohne SKI-Extension nutzen RFC 5280 §4.2.1.2 Methode (1) — SHA-1 über die BIT STRING des Subject Public Keys.
  • Ein Credential erfüllt die Abfrage, wenn der Authority Key Identifier in seiner Ausstellungskette einem der aufgeführten Werte entspricht.
  • Der Verifier befüllt die Liste aus dem tenant-lokalen Trust-Store unter App_Data/<tenant>/trust/vc-issuers/ (bevorzugt) oder trust/mdoc-issuers/ (Fallback). Ein Soft-Cache (45 Sekunden mit mtime-Invalidierung) hält den heissen Pfad schnell; neue Anker sind ohne IIS-Neustart wirksam.
  • Der Tenant-appsettings-Key dcqlTrustedAuthoritiesMode steuert die Ausgabe: all (Standard — jeder Anker im Store), explicit (Whitelist aus App_Data/<tenant>/trust/dcql-trusted-akis.txt, ein b64url-AKI pro Zeile) oder off (Feld weglassen für Wallets, die es ablehnen).

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.

HAIP Final vs OpenID4VP-Legacy. Verschlüsseltes direct_post.jwt ist der HAIP 1.0-Final-Default und die empfohlene Form. Einfaches direct_post wird als OpenID4VP-Legacy-Fallback für Nicht-HAIP-Wallets akzeptiert. Beide sind unten dokumentiert. Verschlüsselte Antworten nutzen ECDH-ES-Schlüsselvereinbarung mit ephemeren P-256-Schlüsseln pro Request und A128GCM- oder A256GCM-Content-Encryption.

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- oder A256GCM-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/A256GCM), 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://www.aloaha.com",
  "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://www.aloaha.com/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).
Warum HA1 (MD5) auf der Leitung? HA1 (MD5 von user:realm:password) ist das Draht-Format gemäß RFC 2617 / RFC 7616 HTTP Digest Authentication und wird hier ausschließlich für die SIP-Hardphone-Interoperabilität verwendet. Es ist nicht der Passwort-Hash unserer Identitätsplattform. OIDC-Anmeldung, EU-Wallet-Anmeldung und Passkey-Flüsse berühren HA1 nicht. Deployments ohne SIP-Digest-Bedarf können den account=password-Pfad vollständig deaktivieren.

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

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://www.aloaha.com/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.aloaha.com/signal.ashx?account=get \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# -> { user: "eu_3f8a1d7e", role: "guest", has_password: false,
#      realm: "www.aloaha.com", ... }
USER="eu_3f8a1d7e"
REALM="www.aloaha.com"

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.aloaha.com/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.aloaha.com/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.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 die OpenID4VP-Legacy-Kompatibilität
#    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):

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

CFG haipStrictMode · Strict-Mode-Schalter für vp-response #

HAIP 1.0 Final schreibt verschlüsseltes direct_post.jwt für VP-Antworten vor. Klartext direct_post (form‑kodiertes vp_token) wird als OpenID4VP-Legacy-Fallback für Nicht‑HAIP-Wallets akzeptiert. Der Strict-Mode-Schalter erlaubt es Operatoren, die Legacy-Form abzulehnen und nur verschlüsselte Antworten zuzulassen.

Tenant-Voreinstellung

JSON-Schlüssel haipStrictMode in App_Data/<tenant>/appsettings.json:

{
  "haipStrictMode": true
}

Akzeptierte Werte: true / false, "1" / "0", "yes" / "no", "on" / "off". Fehlt der Schlüssel, ist der eingebaute Default true. false lässt sowohl verschlüsselte als auch Klartext-Antworten zu (Legacy-Wallet-Kompatibilität).

Pro‑Session-Übersteuerung

Aufrufer von vp-start können den Tenant-Default für eine einzelne Session mit ?strict_haip=1 oder ?strict_haip=0 übersteuern. Der aufgelöste Wert wird beim Start auf dem Session-Record festgeschrieben; das Wallet kann ihn nicht ändern. Nützlich für A/B-Tests, Integrator-Diagnose und Pro-Flow-Lockerungen ohne Änderung der Tenant-Konfiguration.

# Strict für eine Session erzwingen (unabhängig vom Tenant-Default)
curl 'https://phone.aloaha.com/oidc.ashx?action=vp-start&strict_haip=1'

# Strict für eine Session lockern (unabhängig vom Tenant-Default)
curl 'https://phone.aloaha.com/oidc.ashx?action=vp-start&strict_haip=0'

Ablehnungsantwort (strict + Klartext)

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": "invalid_request",
  "error_description": "HAIP strict mode requires encrypted direct_post.jwt response; plain direct_post rejected"
}

Observability

Jede Entscheidung hinterlässt greppbare Trace-Zeilen: [HAIP-STRICT-DIAG] vp-start ... effective=<bool> source=<default|tenant-cfg|query-override> beim Start, [HAIP-STRICT-DIAG] vp-response-check ... bei jedem Wallet-POST und eines von [HAIP-STRICT-DIAG] REJECT|ACCEPT-JWE|ACCEPT-PLAIN je Entscheidung. Reservierte Metriknamen daneben: haip.vp.strict-reject, haip.vp.strict-accept-jwe, haip.vp.strict-accept-plain.

Strict-Mode ändert nicht den JAR, den das Wallet von vp-request holt — das Request-Object annonciert bereits response_mode: direct_post.jwt. Strict-Mode ändert nur, was der Verifier zurückakzeptiert.

Verwandte Pro‑Session-Übersteuerungen — ?lotl_require_chain= und ?status_list_check=

Zwei weitere Pro‑Session-Schalter folgen demselben Muster wie ?strict_haip=. Alle drei werden beim vp-start aufgelöst, auf dem Session-Record festgeschrieben und beim vp-response ausgewertet. Das Wallet kann sie nicht ändern.

# LOTL-Chain-Enforcement für eine Session überspringen (Smoke-Tests / synthetische Aussteller)
curl 'https://phone.aloaha.com/oidc.ashx?action=vp-start&lotl_require_chain=0'

# LOTL-Chain-Enforcement für eine Session erzwingen (unabhängig vom Tenant)
curl 'https://phone.aloaha.com/oidc.ashx?action=vp-start&lotl_require_chain=1'

# OAuth-Status-List-Fetch für eine Session überspringen (Credential ohne status_list)
curl 'https://phone.aloaha.com/oidc.ashx?action=vp-start&status_list_check=skip'

# Nur-Log-Status-Prüfung (widerrufene/suspendierte VCs nicht ablehnen)
curl 'https://phone.aloaha.com/oidc.ashx?action=vp-start&status_list_check=warn'
  • lotl_require_chain akzeptiert 1/0, true/false, yes/no, on/off. Fehlt der Parameter, gilt Oidc:LotlRequireChain (Standard true).
  • status_list_check akzeptiert enforce, warn, skip. Fehlt der Parameter, gilt Oidc:StatusListCheckMode (Standard enforce).
  • Ungültige Werte werden fail‑closed mit HTTP 400 invalid_request abgelehnt (RASP; Länge auf 16 Zeichen begrenzt, Allowlist erzwungen).
  • Produktionsprofil-Sperre. Wenn der Tenant eudiProductionProfile=true oder dcqlTrustedAuthoritiesMode=all setzt, werden alle drei Pro‑Session-Übersteuerungen IGNORIERT und auf sichere Defaults zurückgesetzt (strict, true, enforce). Jeder ignorierte Versuch löst ein Compliance-Audit-Event aus (lotl-chain-compliance-override bzw. statuslist-check-compliance-override) plus LOUD [LOTL-CHAIN-DIAG] COMPLIANCE-OVERRIDE / [STATUSLIST-DIAG] COMPLIANCE-OVERRIDE-Trace.
  • Reservierte Metriknamen: lotl.chain.session-override-{applied,blocked-prod-profile}, statuslist.check.session-override-{applied,blocked-prod-profile}.

GET /.well-known/security.txt #

Pro‑Tenant RFC 9116 security.txt. Wird vom selben oidc.ashx-Handler geliefert, der auch Discovery + JWKS + OpenID Federation bedient; die kanonische URL ist tenant-spezifisch (aus dem Host-Header abgeleitet), sodass Multi‑Tenant-Deployments keine gemeinsame Datei brauchen.

Konfiguration

Eine security-txt.json-Datei unter App_Data/<tenant>/ ablegen:

{
  "contact":             "security@example.com",
  "encryption":          "https://example.com/pgp.asc",
  "acknowledgments":     "https://example.com/security-thanks",
  "policy":              "https://example.com/security-policy",
  "preferred_languages": "en, de"
}

Alle Schlüssel sind optional. Wenn contact ein Schema enthält (mailto:, https:, tel:), wird der Wert unverändert emittiert; eine reine Adresse wird mit mailto: präfixiert. Werte werden gefiltert (CR/LF entfernt, auf 512 Zeichen begrenzt), um Header-Injection über die Operator-Konfiguration zu verhindern.

Antwort — konfigurierter Tenant

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Cache-Control: no-store
X-Content-Type-Options: nosniff

# CodeB Sovereign Communications -- per-tenant security.txt
# Tenant: www.example.com
# Generated: 2026-07-26T10:00:00Z
# Format: RFC 9116

Contact: mailto:security@example.com
Expires: 2027-07-26T10:00:00Z
Canonical: https://www.example.com/.well-known/security.txt
Preferred-Languages: en, de
Policy: https://example.com/security-policy
Encryption: https://example.com/pgp.asc
Acknowledgments: https://example.com/security-thanks
  • Expires wird bei jedem Request als jetzt + 1 Jahr berechnet. Operator-Konfiguration vor Jahresablauf aktualisieren, damit die Datei RFC-konform bleibt.
  • Canonical nutzt den Request-Host (unter Berücksichtigung von X-Forwarded-Proto). Jede Tenant-Domain hat damit ihre eigene kanonische URL.
  • Fehlt preferred_languages, wird en gesetzt; fehlt policy, wird https://<host>/privacy.html gesetzt.

Antwort — Tenant ohne security-txt.json

HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Cache-Control: no-store

# security.txt not configured for this tenant

Der Verifier erfindet bewusst keine Kontaktadresse, wenn keine Konfiguration existiert. Eine Sicherheitskontaktadresse zu veröffentlichen, die nicht überwacht wird, ist schlechter als keine.

RASP

  • Nur GET wird akzeptiert; andere Methoden liefern 405 Method Not Allowed mit Allow: GET.
  • Requests mit Body liefern 400.
  • User‑Agent-Header über 1024 Zeichen liefern 400.

Jeder Zweig emittiert einen [SECTXT-DIAG]- oder [RASP-SECTXT-*]-Trace, damit Operatoren das Audit-Log pro Session greppen können.

POST /oidc.ashx?action=data-deletion-request #

ARF 3.0 Technical Specification 7. Betroffene Personen (oder ein Verantwortlicher / eine Aufsichtsbehörde in ihrem Namen) können die Löschung ihrer European Digital Identity Wallet Sitzungsdaten und Audit-Einträge beim jeweiligen Tenant beantragen. Der Antrag wird für die Prüfung durch einen menschlichen Operator eingereiht; es wird nichts automatisch gelöscht.

Request-Body

{
  "sub":             "<opake Subject-ID aus einer früheren VP-Antwort>",
  "reason":          "<Freitext, optional, max. 512 Zeichen>",
  "requester_type":  "self" | "controller" | "dpa",
  "contact":         "user@example.com  ODER  https://example.com/callback",
  "wallet_attestation": "<WA-JWT, optional, max. 8 KB>"
}

Antwort — eingereiht (HTTP 202)

{
  "request_id":  "<opak>",
  "status":      "queued",
  "eta_days":    30,
  "controller":  "<DPO-Kontakt aus tenant-security.txt sofern konfiguriert>"
}

Der Antrag wird unter App_Data/<tenant>/data-deletion-queue/<request_id>.json persistiert (atomarer Write + .backup-Sidecar). Eine Meta-Audit-Zeile wird pro Monat an App_Data/<tenant>/audit/deletion-requests-YYYY-MM.log.jsonl angehängt.

RASP

  • Body-Cap 4 KB → 400 / 413.
  • contact validiert: nur E-Mail-Adresse oder https://-URL. file://, javascript:, http://, tel:, mailto:, Whitespace → 400.
  • Rate-Limit pro IP: 3 Anfragen pro Stunde. Überzahl → 429 mit Retry-After: 3600.
  • Warteschlangen-Voll-Schutz: sobald App_Data/<tenant>/data-deletion-queue/ Ts7:DeletionQueueMaxEntries Dateien enthält (Default 10000) → 503.
  • Tenant-Opt-out: Ts7:DisableDeletionRequests=true in appsettings.json führt zu 404. Default ist aktiviert.

Operator-Listing

Die Warteschlange ist über GET /oidc.ashx?action=data-deletion-list einsehbar, beschränkt auf Authorization: Bearer <Admin-JWT> oder X-CodeB-Admin-Signature. Antwort enthält pro Eintrag request_id, subject_hash, requester_type, status und created_at (max. 500 pro Aufruf).

Discovery

Die URL des Endpoints ist im tenant-eigenen /.well-known/openid-configuration unter dem benutzerdefinierten Feld data_deletion_request_endpoint angekündigt.

Jeder Aufruf emittiert einen [TS7-DELETION-DIAG]-Trace bei Entry, Queued-Event und in jedem RASP-Zweig ([RASP-TS7-*]). Reservierte Metrik-Namen: ts7.deletion.requested, ts7.deletion.processed. Dies ist eine Schnittstelle für Betroffenenrechte, kein Versprechen sofortiger Löschung — jeder Antrag durchläuft einen Operator-Review-Workflow.

GET /oidc.ashx?action=dpa-report #

ARF 3.0 Technical Specification 8. Superuser-exklusiver Export der Verarbeitungseinträge, die zu einer einzelnen betroffenen Person auf diesem Tenant gespeichert sind. Vorgesehen für Auskunftsersuchen nach Artikel 15 DSGVO und Untersuchungen nach Artikel 58 DSGVO durch Datenschutzaufsichtsbehörden.

Request

GET /oidc.ashx?action=dpa-report&subject=<sub>
                                &from=YYYY-MM-DD    (optional)
                                &to=YYYY-MM-DD      (optional)
                                &format=json         (Default; pdf deferred)
                                &purpose=<Freitext>  (optional, max. 256)

Authorization: Bearer <Admin-JWT>
        ODER
X-CodeB-Admin-Signature: <HMAC-SHA256 hex>

Standard-Zeitraum ist die letzten 12 Monate; explizite from/to-Werte werden bei 24 Monaten gedeckelt. Alles darüber wird still geklemmt und als [TS8-DPA-DIAG] span-capped geloggt.

Antwort (HTTP 200, application/json)

{
  "report_id":     "<opak>",
  "generated_at":  "2026-07-26T12:00:00Z",
  "tenant":        "<tenant-fqdn>",
  "subject":       "<sub>",
  "period":        { "from": "...", "to": "..." },
  "records": [
    { "kind": "vp-response",   "at": "...", "status":        "..." },
    { "kind": "vc-issuance",   "at": "...", "vct":           "...", "credential_id": "..." },
    { "kind": "audit-login",   "at": "..." },
    { "kind": "audit-revoke",  "at": "..." }
  ],
  "aggregate": {
    "vp_responses":      N,
    "vc_issuances":      N,
    "sign_ins":          N,
    "revocation_checks": N
  },
  "truncated":     false,
  "controller":    "<DPO-Kontakt des Tenants sofern konfiguriert>",
  "meta_audit":    { "generated_by": "<Hash>", "purpose": "<Freitext>" }
}

Datenquellen

  • App_Data/<tenant>/vp-sessions/*.json gefiltert nach UserHint / ClientRef.
  • App_Data/<tenant>/vci-issuance/*.jsonl gefiltert nach sub.
  • App_Data/<tenant>/audit/*.log.jsonl gefiltert nach user. Eigene Meta-Audit-Dateien (dpa-reports-*, deletion-requests-*) werden ausgeschlossen, um Rekursion zu vermeiden.

Jeder Report-Aufruf wird in App_Data/<tenant>/audit/dpa-reports-YYYY-MM.log.jsonl mit Report-ID, Subject-Hash, Zweck und Anfrager-IP protokolliert.

RASP

  • Alles ausser GET405 mit Allow: GET.
  • Fehlende / ungültige Auth → 401.
  • Response-Body-Cap 10 MB. Bei Überlauf enthält die Antwort truncated:true mit Hinweis, den Zeitraum einzugrenzen.
  • format=pdf liefert 501 Not Implemented. PDF-Export ist zurückgestellt.

Jeder Zweig emittiert einen [TS8-DPA-DIAG]- oder [RASP-TS8-*]-Trace. Reservierter Metrik-Name: ts8.dpa.report.generated. Der Endpunkt ist Betroffenenrechts-Infrastruktur, keine allgemeine Analytik; jeder Aufruf ist auditfähig.

POST /vci.ashx?credential · ARF TS3 Key-Attestation-Annahme #

Zusätzlich zur Wallet Instance Attestation (die belegt, dass die Wallet-Software selbst vertrauenswürdig ist) kann eine European Digital Identity Wallet eine separate Key Attestation beilegen. Sie belegt, dass der konkrete Schlüssel, mit dem der Besitz-Nachweis erbracht wird, in einer vertrauenswürdigen sicheren Umgebung (StrongBox, Secure Enclave o. ä.) erzeugt wurde. Das ist ARF 3.0 Technical Specification 3 — das „Wallet Trust“-Profil.

Wo die Wallet sie ablegt

Die Wallet bettet das Key-Attestation-JWT in das OpenID4VCI-Proof-JWT ein, das sie an POST /vci.ashx?credential sendet. Zwei Positionen werden akzeptiert:

  • JOSE-Header-Slot: "key_attestation": "<kompakter JWS>".
  • Payload-Array: "attestations": ["<kompakter JWS>", ...]. Nur der erste Eintrag wird ausgewertet.

Was wir akzeptieren

  • Generische JWT-basierte Key Attestations (RFC 7515 Compact JWS). Hersteller-spezifische Formate (Android android-key, Apple apple-anonymous, TPM) werden nicht geparst — sie sind in ein JWS zu verpacken, dessen Payload die attestierten Schlüssel und das Format-Tag auflistet.
  • alg aus ES256, ES384, ES512.
  • typ = "key-attestation+jwt" falls vorhanden (weicher Check).
  • x5c-Header mit einer Kette bis zu 5 Zertifikaten, die auf einem per-Tenant-Trust-Anchor endet.
  • Payload: iss, iat innerhalb der letzten 24 Stunden (±60 s Skew), exp nicht abgelaufen, attested_keys-Array mit bis zu 10 JWKs; ein Eintrag muss den Proof-of-Possession-Schlüssel per RFC 7638-Thumbprint treffen.

Per-Tenant-Konfiguration

App_Data/<tenant>/appsettings.json:

{
  "Vci:KeyAttestationMode": "optional"   /* off | optional (Default) | require */
}

Trust-Anchors: die Zertifikate der Attestation-Issuer als *.pem (auch *.cer oder *.crt) in App_Data/<tenant>/trust/key-attestation-issuers/ ablegen. Anchors werden bei jedem Credential-Request gelesen; kein Neustart nötig.

Metadaten-Ankündigung

GET /vci.ashx?metadata enthält jetzt:

{
  "key_attestation_signing_alg_values_supported": ["ES256", "ES384", "ES512"],
  "key_attestation_types_supported": ["key-attestation+jwt", "key-attestation+jades"],
  "key_attestations_required": false,
  "wallet_attestation_types_supported": ["wallet-instance"],
  "wallet_attestation_signing_alg_values_supported": ["ES256", "ES384", "ES512"]
}

Der Boolean key_attestations_required spiegelt Vci:KeyAttestationMode (true nur bei require).

JAdES Baseline B (ETSI TS 119 182-1)

Gemäss den Durchführungsvorschriften der European Digital Identity Wallet vom Juli 2026 KANN eine Key Attestation ein JWS sein, dessen Protected Header die JAdES-Baseline-B-Header-Claims aus ETSI TS 119 182-1 trägt. Der Verifier akzeptiert sowohl die schlichte typ = "key-attestation+jwt"-Form als auch die JAdES-Form typ = "key-attestation+jades" auf demselben Code-Pfad.

  • crit — Array von Extension-Header-Claim-Namen, die der Verifier verstehen MUSS. Jeder Name wird gegen die erkannte Menge (alg, typ, kid, cty, x5c, x5t, x5t#S256, sigT, sigPl, sigX5ts, srCms, srAts, srCommitment, adoTst, sigPId, crit) geprüft. Unbekannt → jades_crit_unrecognised. Längen-Cap 20 Einträge (RASP).
  • sigT — RFC 3339-Signaturzeit. Wenn abwesend, wird gewarnt; wenn die Drift zu iat 5 Minuten überschreitet, wird mit jades_sigt_iat_skew abgelehnt.
  • x5t#S256 — base64url-SHA-256 des Leaf-Zertifikats in x5c. Wenn vorhanden, hasht der Verifier x5c[0].RawData und lehnt mit jades_x5t_s256_mismatch bei jeder Abweichung ab. Starke Anchor-Pin-Verteidigung gegen Chain-Swap.
  • sigPl — Signatur-Policy-Identifier. Wird nur im Diag-Log erfasst, nicht gegated (Strict-Policy-Modus ist zukünftige Arbeit).

Erfolgreiche Validierung emittiert [KEY-ATTESTATION-DIAG] jades-validated typ=… x5t-pinned=… crit-count=… sigT-present=… sealed=…. Das ist die primäre Beweiskette für die JAdES-Header-Claims.

Sealed-vs-Signed-Zertifikat (Opt-in)

Die Durchführungsvorschriften vom Juli 2026 unterscheiden ein Wallet-Provider-Signatur-Zertifikat (eIDAS-Advanced-Signatur) von einem Wallet-Provider-Siegel-Zertifikat (eIDAS-Electronic-Seal, ETSI EN 319 412-1). Ein Siegel-Zertifikat trägt die X.509-Extended-Key-Usage-OID 0.4.0.194112.1.4 (id-kp-EIDASSeal). Mit "Vci:KeyAttestationRequireSealed": true in App_Data/<tenant>/appsettings.json wird diese OID am Leaf-Zertifikat verlangt; der Default ist false, sodass Signatur- und Siegel-Zertifikate gleichermassen akzeptiert werden. Wenn erzwungen, wird eine Kette ohne Seal-EKU mit cert_not_sealed abgelehnt und als [KEY-ATTESTATION-DIAG] reject reason=jades-cert-not-sealed geloggt.

RASP

  • Attestation-JWT-Länge maximal 32 KB.
  • alg-Allowlist strikt ES256/ES384/ES512.
  • iat-Fenster: innerhalb der letzten 24 Stunden + 60 s Skew.
  • x5c-Kettenlänge maximal 5 Zertifikate.
  • attested_keys-Array maximal 10 Einträge.
  • crit-Array maximal 20 Einträge; sigT-vs-iat-Drift maximal 5 min; x5t#S256-Mismatch scheitert geschlossen.
  • Fehlende Trust-Anchors bei anwesender Attestation → invalid_key_attestation / no_anchors_configured. Fehlende Attestation bei require-Modus → key_attestation_required.

Jeder Zweig emittiert einen [KEY-ATTESTATION-DIAG]- oder [RASP-KEY-ATTESTATION]-Trace. Reservierte Metrik-Namen: vci.key-attestation.required, vci.key-attestation.accepted, vci.key-attestation.rejected, vci.key-attestation.skipped.

POST /vci.ashx?nonce · OID4VCI 1.0 Final — dedizierter Nonce-Endpunkt #

OpenID for Verifiable Credential Issuance 1.0 Final (nach Draft 15 veröffentlicht) definiert in Abschnitt 7.2 einen dedizierten Nonce-Endpunkt. Vor Final konnte der Issuer eine c_nonce ausschließlich in einer Token-Response oder einer Credential-Error-Response ausliefern. Mit dem Nonce-Endpunkt kann eine European Digital Identity Wallet jederzeit eine frische Replay-Schutz-Nonce anfordern — typischerweise, um den Proof-of-Possession-JWT parallel zur Token-Anfrage vorzubereiten oder nach einem Credential-Retry sauber neu zu starten.

Request

POST /vci.ashx?nonce — leerer Body. Öffentlich: kein Bearer-Token erforderlich. GET wird zusätzlich akzeptiert; POST ist die HAIP-konforme Variante.

Response

HTTP/1.1 200 OK
Cache-Control: no-store
Content-Type: application/json; charset=utf-8

{
  "c_nonce": "R2X4kV6QjD8u…",
  "c_nonce_expires_in": 300
}

Die Wallet verwendet diese c_nonce als nonce-Claim im nächsten Proof-of-Possession-JWT gegenüber /vci.ashx?credential. Der Issuer hält die Nonce in einem pro-Tenant-Standalone-Store (5 Minuten TTL, harte Kapazität 5000 Einträge) und verbraucht sie bei erstem Treffer — Single-Use Replay-Schutz.

Metadaten-Aufnahme

{
  "nonce_endpoint":        "https://<tenant>/vci.ashx?nonce",
  "notification_endpoint": "https://<tenant>/vci.ashx?notification",
  "spec_version":          "OID4VCI 1.0"
}

RASP

  • Methode-Allowlist: nur POST und GET; alles andere → 405 method_not_allowed.
  • Pro-IP Sliding-Window-Ratenlimit (60 Requests/Minute, gemeinsam mit den anderen öffentlichen Wallet-Pfaden).
  • Request-Body wird geleert und verworfen — der Endpunkt akzeptiert keine Eingabe.
  • Response setzt Cache-Control: no-store und Pragma: no-cache, damit Zwischen-Caches keine Nonces wiederverwenden.

Jeder Zweig emittiert einen [VCI-NONCE-DIAG]-Trace: entry, issued, proof-check. Reservierter Metrik-Name: vci.nonce.issued. Verwendet eine Wallet eine Nonce, die dem Standalone-Store entstammt, protokolliert HandleCredential src=standalone-consumed; der Token-gebundene Pfad protokolliert src=token-bound.

POST /vci.ashx?notification · OID4VCI 1.0 Final — Notification-Endpunkt #

OpenID for Verifiable Credential Issuance 1.0 Final Abschnitt 10 führt einen Rückkanal Wallet-zu-Issuer ein. Nachdem die Wallet ein frisches Credential gespeichert (oder abgelehnt) hat, sendet sie einen kurzen Statushinweis, damit der Issuer den Credential-Lifecycle nachhalten kann. Eine European Digital Identity Wallet meldet damit die Ergebnisse accepted, deleted oder failure.

Request

POST /vci.ashx?notification HTTP/1.1
Authorization: DPoP eyJhbGciOiJFUzI1NiI…
Content-Type: application/json

{
  "notification_id": "9f83bcc0-4e1a-4c3f-8d2f-1b0a3d7c5e12",
  "event": "credential_accepted",
  "event_description": "Im Secure Enclave gespeichert"
}

Response

HTTP/1.1 204 No Content bei Erfolg. Kein Response-Body.

Authentifizierung

Die Wallet MUSS ein Bearer- oder DPoP-Access-Token aus einem vorangegangenen /vci.ashx?token-Aufruf mitschicken. Das Token muss noch innerhalb seiner 5-Minuten-TTL liegen, zum selben Tenant gehören und im aktiven Access-Token-Index des Issuers geführt sein. Fehlende / unbekannte / abgelaufene / tenantfremde Tokens → 401 invalid_token.

Persistenz

Jede angenommene Notification wird an App_Data/<tenant>/vci-notifications/<YYYY-MM>.log.jsonl angehängt. Ein JSON-Objekt pro Zeile; monatlicher Rollover hält einzelne Dateien klein. Der Eintrag enthält einen 12-Zeichen-Token-Fingerprint (niemals das Rohtoken), die Notification-ID, das Event, die bereinigte Event-Beschreibung sowie offer_id und client_id soweit verfügbar. Auditfähige Evidenz über Wallet-seitige Ergebnisse im Credential-Lifecycle.

RASP

  • Methode-Allowlist: nur POST.
  • Body-Obergrenze: 2 KB (Header-Vorprüfung Content-Length und Post-Read-Längenprüfung).
  • notification_id: 16–128 Zeichen, Allowlist [A-Za-z0-9_-].
  • event: strikte Enumeration — credential_accepted, credential_deleted oder credential_failure. Sonst → 400 invalid_notification_request.
  • event_description: kürzung auf 256 Zeichen; CR/LF/TAB entfernt; nicht druckbare ASCII-Bytes ersetzt durch ? — Log-Injection-Verteidigung.
  • Access-Token geprüft gegen den gleichen _accessTokens-Index, den auch /vci.ashx?credential verwendet; Tenant-Scoping wird erzwungen.
  • Pro-IP Sliding-Window-Ratenlimit gemeinsam mit den anderen öffentlichen Wallet-Pfaden.

Jeder Zweig emittiert einen [VCI-NOTIF-DIAG]-Trace. Reservierte Metrik-Namen: vci.notification.accepted, vci.notification.invalid.

CONFIG Client-bezogene Wallet-Claim-Allowlist #

Sobald eine Präsentation der European Digital Identity Wallet eintrifft und PID-Claims extrahiert wurden, entscheidet die Betreiberin je OIDC-Client, welche dieser Claims über id_token / userinfo an einen nachgelagerten Consumer weitergereicht werden. Das ist die Haltung nach Art. 25 DSGVO („Datenschutz durch Technikgestaltung und Voreinstellungen“), erzwungen am Token-Mint-Grenzpunkt und nicht dem nachgelagerten Vertrauen überlassen.

Konfigurationsdatei

App_Data/<tenant>/oidc-clients/<client_id>/wallet-claim-allowlist.json

{
  "allow": ["given_name", "family_name", "age_over_18", "email"],
  "deny_reason_default": "wallet_claim_not_in_client_allowlist"
}

Verhalten bei fehlender Datei

  • Tenant-appsettings Oidc:WalletClaimDefaultAllowAll=true → alle wallet-basierten Claims werden durchgereicht (Rückwärtskompatibilitäts-Escape).
  • Standard (Einstellung fehlt oder false) → kein wallet-basierter Claim erreicht den Client. Eine leere allow-Liste wirkt wie eine fehlende Datei: nichts passiert die Grenze.

Enforcement-Punkt

Die Filterung läuft in MergeVcClaims in oidc.ashx, unmittelbar nach der zweikanaligen Zusammenführung (Namespaced-vc-Block plus Top-Level-Lift der Standard-Claims) und vor der Serialisierung der userinfo-Antwort. Admin-gepflegte Profilfelder, die bereits vorher im Claims-Dictionary standen, werden nie gefiltert — nur das wallet-basierte Delta.

RASP

  • Allowlist-Datei auf 8 KB begrenzt; größere Dateien → alle Wallet-Claims verweigert und [RASP-WALLET-CLAIM-ALLOWLIST] reject reason=file-too-big protokolliert.
  • allow-Array auf 32 Einträge begrenzt.
  • Jeder Eintrag muss dem Regex ^[a-z][a-z0-9_]{0,63}$ entsprechen (Standard-OIDC-Claim-Namensform). Abweichende Einträge werden protokolliert und übersprungen.
  • Kein JSON, IO-Fehler oder eine Ausnahme → fail-closed: alle wallet-basierten Schlüssel werden aus der Antwort entfernt.

LOUD-Trace pro Filter-Durchgang: [WALLET-CLAIM-ALLOWLIST-DIAG] filter tenant=<t> client=<cid> src=<file-loaded|no-file-default-deny|no-file-default-allow-all> mode=<allow-all|explicit> requested=<csv> allowed=<csv> denied=<csv>. Reservierte Metrik-Namen: oidc.wallet-claim.filtered, oidc.wallet-claim.denied.

CONFIG PID-Speicherfrist (Retention-TTL) #

Verifizierte Person-Identification-Data-Claims (PID) aus einer European-Digital-Identity-Wallet-Präsentation können im Nutzerdatensatz der Tenant-Instanz auf Platte gehalten werden, damit nachfolgende authentifizierte Sitzungen sie ohne erneuten Wallet-Roundtrip anzeigen können. Art. 5 Abs. 1 lit. c DSGVO (Datenminimierung) und Art. 5 Abs. 1 lit. e DSGVO (Speicherbegrenzung) verlangen, dass diese Persistenz begrenzt und auditierbar ist.

Konfiguration

Tenant-appsettings-Schlüssel Oidc:PidRetentionSeconds (Standard 2592000 = 30 Tage).

  • 2592000 → PID-Claims werden bis zu 30 Tage vorgehalten; ausgelieferter Standard.
  • 0 → PID-Claims werden nie persistiert; der Write-Pfad überspringt den Disk-Schritt komplett und die Wallet-Präsentation ist nur für die Sitzung gedacht.
  • -1 → unbegrenzte Persistenz (Opt-out). Erfordert eudiProductionProfile=false; im Production-Profil wird der Wert ignoriert und auf 30 Tage geklemmt, ein Compliance-Audit-Event wird ausgelöst.

Speicher-Schema

Sobald PID-Claims persistiert werden, wird das Nachbarfeld pid_claims_at (Unix-Sekunden) im gleichen JSON-Dokument mitgeschrieben. Bei späteren Reads vergleicht der Retention-Helper now − pid_claims_at mit der Tenant-TTL; abgelaufene PID-Slots werden im zurückgegebenen Dictionary weggelassen. In dieser Runde gibt es keinen aktiven Sweeper — der Ablauf ist lazy on Read.

Production-Profile-Lock

Ist eudiProductionProfile=true, wird der Wert -1 verweigert. Die effektive TTL wird auf 30 Tage geklemmt, ein [PID-RETENTION-DIAG] COMPLIANCE-OVERRIDE-Trace feuert und ein pid-retention-compliance-override-Audit-Event wird für die Betreiberin geschrieben. Damit bleibt der ehrliche Default für Tenants, die regulierte EU-Dienste ausliefern, erhalten.

LOUD-Traces: [PID-RETENTION-DIAG] written…, [PID-RETENTION-DIAG] expired…, [PID-RETENTION-DIAG] skip-write… und [PID-RETENTION-DIAG] COMPLIANCE-OVERRIDE…. Reservierte Metrik-Namen: oidc.pid.retention.written, oidc.pid.retention.expired-lazy.

CONFIG Kryptographisches-Modul-Architektur #

Jede kryptographische Operation der Aloaha-CSC-v2-Remote-Signaturschicht — Bereitstellung eines pro Tenant dedizierten Signatur-Credentials, Signatur eines Roh-Hashes, Ausstellen und Prüfen des Signature-Activation-Data-JWT (SAD) — läuft über eine schmale ICryptoModule-Schnittstelle. Zweck ist architektonisch: die CSC-v2-Handler wissen nichts darüber, wo das private Schlüsselmaterial lebt, sodass ein späterer Hardware-Austausch (Azure Managed HSM, PKCS#11-HSM on-prem) ohne Veränderung der Handler-Oberfläche möglich ist.

Schnittstelle

ICryptoModule
  string ModuleId              // z.B. "software-dpapi-localmachine"
  string ModuleType            // "software" | "hsm-cloud" | "hsm-onprem" | "tee"
  string ModuleCertification   // null | "EN 419 221-5:2018 cert:<ref>" | "FIPS 140-3 L3 cert:<ref>"

  // Phase 1b (2026-07-27 mega-sprint) -- nutzerbezogene API
  CryptoCredential EnsureCredential(tenant, user, ctx)
  byte[]           SignHash(tenant, user, hash, hashOid, signAlgoOid, ctx)
  string           SignSadJwt(tenant, user, payload, ctx)
  bool             VerifySadJwt(tenant, user, jwt, out payload, out reason, ctx)
  // Phase 1a Legacy-Overloads (user=null) bleiben für HSM-Aufrufer

Nutzerbezogene Credentials (Phase 1b, ausgeliefert 2026-07-27)

Das Software-Modul verwaltet die PFX-Datei pro Nutzer, nicht pro Tenant. Beim ersten credentials/list-Aufruf für einen OIDC-Subject wird App_Data/<tenant>/csc-signer-certs/<sha256(user).first16>.pfx mit CN=<user>, EC P-256, 1 Jahr, KU=digitalSignature+nonRepudiation, EKU=emailProtection lazy angelegt. Nutzer A sieht niemals das Zertifikat oder die credentialID von Nutzer B; das SAD-JWT trägt den OIDC-Subject und signatures/signHash verweigert die Signatur, wenn SAD.sub nicht mit dem Bearer-sub übereinstimmt. Mit Csc:PerUserCerts=false in appsettings lässt sich der Legacy-Pfad (ein Zertifikat pro Tenant) erzwingen. Metrik csc.cert.user-autogenerated zählt jede erste Nutzer-Zert-Erzeugung. Voraussetzung für einen späteren ETSI-EN-319-411-2-CP-2-(QCP-l)-konformen QSCD-Konnektor; das Software-Modul allein erreicht CP-2 nicht (weiterhin nur AdES, kein QSCD, keine Face-to-Face-Enrolment).

SCAL2 Signature Activation Protocol (Phase 1b, ausgeliefert 2026-07-27)

credentials/authorize verzweigt anhand von Csc:DefaultScal (Default 1) oder Query ?scal=1|2. SCAL1 liefert das SAD-JWT sofort in der Antwort. SCAL2 legt einen Pending-Datensatz unter App_Data/<tenant>/csc-pending-sad/<authorization_id>.json an (TTL 300s). Hat der authentifizierte OIDC-Subject bereits Passkeys registriert, enthält die Antwort zusätzlich webauthn_supported: true und ein webauthn_options-Objekt (WebAuthn Level 2 PublicKeyCredentialRequestOptions mit sitzungsgebundener Challenge), das der Client unmittelbar an navigator.credentials.get() übergibt. Ein zweiter Aufruf an credentials/authorize/confirm gibt das SAD nach Vorlage eines Bestätigungsfaktors frei: PIN (Body-Feld pin oder Header X-CSC-Sign-PIN, 4–12 hex/digit) oder WebAuthn-Assertion (Feld webauthn_assertion mit id + response.{clientDataJSON, authenticatorData, signature, userHandle?}) — letztere wird durch CodeB.Passkeys.AuthenticationFacade als vollständige FIDO2-Level-2-RP-Prüfung verifiziert: clientDataJSON.type="webauthn.get", Challenge-Match gegen die server-ausgestellte Challenge, Origin-Match gegen den Tenant-Host, authenticatorData.rpIdHash == SHA-256(rpId), UP- und UV-Flag gesetzt (D-4 verlangt stets User-Verification), Signaturprüfung gegen den gespeicherten COSE-Public-Key (ES256/RS256/EdDSA), Counter-Regression-Check (Anti-Clone) und Einweg-Sitzungsverbrauch (Anti-Replay). Der SAD-Payload-Wert confirm_factor wird webauthn-hwk für hardware-gebundene bzw. webauthn-swk für synchronisierte Credentials gemäß RFC 8176. Pending-Datensätze sind Einweg: erfolgreiche Bestätigung löscht sie, abgelaufene werden abgewiesen und weggeputzt, Bearer-Subject-Mismatch löscht ebenfalls und liefert 403. Metriken: csc.scal2.pending-minted, csc.scal2.confirm-success, csc.scal2.confirm-expired, csc.scal2.confirm-not-found, csc.scal2.confirm-user-mismatch, csc.scal2.confirm-factor-missing, csc.scal2.confirm-webauthn-verified, csc.scal2.confirm-webauthn-rejected. Bei Csc:DefaultScal=2 annonciert credentials/info automatisch SCAL: "2" und PIN: {presence:"true", format:"N", label:"Sign PIN"} gemäß CSC v2 §11.5.

Provider-Wahl

Tenant-appsettings-Schlüssel Csc:CryptoModuleProvider. Akzeptierte Werte:

  • softwareStandard, ausgeliefert. Umschließt die dreistufige Signatur-Zertifikats-Lifecycle (PFX auf Platte → wacs-ausgestelltes Zertifikat → auto-generiertes ECDsa-P-256-selbstsigniertes Leaf). Das private Schlüsselmaterial liegt im Windows-CNG-Key-Container gebunden an den LocalMachine-Key-Set; das PFX-Passwort auf Platte ist mit DPAPI im LocalMachine-Scope gewickelt. Keine Hardware-Root-of-Trust, keine Manipulationssicherheit, keine Protection-Profile-Zertifizierung.
  • hsm-azurekv — reserviert für den Azure-Managed-HSM-Konnektor. In diesem Build nicht implementiert. Anforderungen liefern HTTP 501 mit error=crypto_module_not_configured.
  • hsm-pkcs11 — reserviert für den PKCS#11-Konnektor on-premises. In diesem Build nicht implementiert. Anforderungen liefern HTTP 501 mit error=crypto_module_not_configured.

Ehrliche Offenlegung über credentials/info

Die Antwort credentials/info in CSC v2 wurde um drei Felder im key-Objekt erweitert — CSC v2 §11.5 erlaubt Erweiterungen; die Basisfelder (status, algo, len, curve) sind unverändert. Die Ergänzungen sind eine ehrliche Beschreibung dessen, was den Signaturschlüssel absichert:

{
  ...
  "key": {
    "status": "enabled",
    "algo": ["1.2.840.10045.4.3.2"],
    "len": 256,
    "curve": "1.2.840.10045.3.1.7",
    "protection":          "software-dpapi-localmachine",
    "moduleType":          "software",
    "moduleCertification": null
  },
  ...
}

Ein HSM-gestütztes Deployment würde z.B. protection="hsm-cloud:azurekv:<vault>", moduleType="hsm-cloud", moduleCertification="EN 419 221-5:2018 cert:<ref>" liefern. Wallets, die diese Genauigkeit zur Vertrauensentscheidung benötigen, können diese Felder ohne Protokolländerung auswerten.

Standards-Ausrichtung

ETSI TS 119 431-1 Klausel 5 beschreibt die Modul-Grenze eines Vertrauensdiensteanbieters, der Signaturen in einem Server-seitigen Modul erzeugt. Unser ICryptoModule materialisiert die vier Operationen, die der CSC-v2-Flow benötigt. Was das Software-Modul ausdrücklich nicht leistet: Modul-Zertifizierung (EN 419 221-5, FIPS 140-2/3), Manipulationsevidenz, physische Sicherheitskontrollen oder QSCD-Bindung für qualifizierte elektronische Signaturen. Das sind HSM-Angelegenheiten und gehören in eine spätere Phase.

ETSI TS 119 432-1 (Signature Activation Protocol, SAP): Phase 1b liefert den zweistufigen SCAL2-Flow mit zwei Bestätigungsfaktor-Pfaden: (1) PIN (hex/digit, 4–12 Zeichen) und (2) WebAuthn / FIDO2 Level 2, komplett verifiziert durch CodeB.Passkeys.AuthenticationFacade (Challenge-/Origin-/RP-ID-Bindung, UP+UV-Erzwingung, Signaturprüfung gegen den gespeicherten COSE-Public-Key, Counter-Regression-Check, Einweg-Sitzungsverbrauch). Der Confirm-Endpoint löscht Pending-Datensätze einmalig und weist Replay-SADs zurück, deren sub-Claim nicht mit dem Bearer-Subject übereinstimmt.

LOUD-Traces je Zweig: [CRYPTOMOD-DIAG] factory tenant=<t> provider=<software|hsm-azurekv|hsm-pkcs11>, [CRYPTOMOD-DIAG] EnsureCredential ok tenant=<t> userSlug=<16hex|*> module=<m> credentialId=<...>, [CSC-SCAL2-DIAG] pending-minted tenant=<t> authId=<...>, [CSC-SCAL2-DIAG] confirm-success tenant=<t> authId=<...> factor=<pin|webauthn-hwk|webauthn-swk>, [CRYPTOMOD-DIAG] SignHash tenant=<t> alg=<ES256|RS256> hashLen=32, [CRYPTOMOD-DIAG] SignSadJwt tenant=<t> alg=<...> kid=<...>, [CRYPTOMOD-DIAG] VerifySadJwt tenant=<t> alg=<...> result=<OK|reject:<reason>>. RASP: SignHash weist Hashes ungleich 32 Byte ab; EnsureCredential scheitert geschlossen, wenn alle drei Software-Modul-Tiers versagen; HSM-Provider scheitern geschlossen mit crypto_module_not_configured.

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.