Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
96be14f
Add Consumer API OpenID4VP verifier site
tnotheis Sep 3, 2026
8a3317a
Extend reference onboarding for VP tokens
tnotheis Sep 7, 2026
8c3b176
Remove verifier mock credential shell
tnotheis Sep 7, 2026
2530e77
Render verifier markup from Razor
tnotheis Sep 7, 2026
f8d1fc1
Build verifier assets in Docker node stage
tnotheis Sep 7, 2026
43dff45
Merge branch 'main' into feat/openid4vp-verifier-site
mergify[bot] Sep 7, 2026
a441547
fix: make vp show
tnotheis Sep 7, 2026
3e8d6db
chore: some style fixes
tnotheis Sep 7, 2026
905e99b
fix: show all claims from token
tnotheis Sep 7, 2026
855a87d
chore: set CORS allowerdOrigins to *
tnotheis Sep 8, 2026
943572e
chore: improve design
tnotheis Sep 8, 2026
2856ab4
chore: delete fallback creation of html elements
tnotheis Sep 8, 2026
3a25942
feat: add signature validation
tnotheis Sep 8, 2026
7c8f0b9
test: add tests for validation logic
tnotheis Sep 8, 2026
9b63b9e
feat: translate all texts to German
tnotheis Sep 8, 2026
60cf2e2
ci: run unit tests in pipeline
tnotheis Sep 9, 2026
dbee4f2
chore: make error messages less technical
tnotheis Sep 9, 2026
59581c9
chore: add AGENTS.md file
tnotheis Sep 9, 2026
5b79be5
Merge branch 'main' into feat/openid4vp-verifier-site
tnotheis Sep 10, 2026
8c39e3e
chore: translate AGENTS.md to English
tnotheis Sep 10, 2026
270f12b
chore: improve error messages
tnotheis Sep 10, 2026
4b4115d
fix: reject unbound standalone credentials
tnotheis Sep 10, 2026
4c9024d
fix: scope OpenID4VP verifier styles
tnotheis Sep 10, 2026
29e03bd
fix: restore onboarding when verifier startup fails
tnotheis Sep 10, 2026
cf971cc
Merge branch 'main' into feat/openid4vp-verifier-site
mergify[bot] Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ jobs:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: ${{ runner.os }}-nuget-
- name: Install OpenID4VP Verifier dependencies
working-directory: Applications/ConsumerApi/src/OpenId4VpVerifierSite
run: npm ci
- name: Run OpenID4VP Verifier unit tests
working-directory: Applications/ConsumerApi/src/OpenId4VpVerifierSite
run: npm test
- name: Run tests
run: ./.ci/test.sh
env:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
Applications/ConsumerApi/src/OpenId4VpVerifierSite/dist/
Applications/ConsumerApi/src/wwwroot/openid4vp-verifier/

# Visual Studio 6 build log
*.plg
Expand Down
27 changes: 27 additions & 0 deletions Applications/ConsumerApi/src/ConsumerApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

<PropertyGroup>
<UserSecretsId>f114fba8-95dd-4fee-8385-af8e8a343c68</UserSecretsId>
<OpenId4VpVerifierSiteRoot>$(MSBuildProjectDirectory)/OpenId4VpVerifierSite/</OpenId4VpVerifierSiteRoot>
<OpenId4VpVerifierDistRoot>$(OpenId4VpVerifierSiteRoot)dist/openid4vp-verifier/</OpenId4VpVerifierDistRoot>
<OpenId4VpVerifierWwwRoot>$(MSBuildProjectDirectory)/wwwroot/openid4vp-verifier/</OpenId4VpVerifierWwwRoot>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -42,4 +45,28 @@
<Delete Files="$(ProjectDir)appsettings.override.json" />
<Copy SourceFiles="..\..\..\appsettings.override.json" DestinationFolder="$(ProjectDir)" UseHardlinksIfPossible="true" />
</Target>

<Target Name="InstallOpenId4VpVerifierSiteDependencies"
Inputs="$(OpenId4VpVerifierSiteRoot)package-lock.json"
Outputs="$(OpenId4VpVerifierSiteRoot)node_modules/.package-lock.json"
Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipOpenId4VpVerifierBuild)' != 'true'">
<Exec WorkingDirectory="$(OpenId4VpVerifierSiteRoot)" Command="npm ci" />
</Target>

<Target Name="BuildOpenId4VpVerifierSite"
BeforeTargets="ResolveProjectStaticWebAssets"
DependsOnTargets="InstallOpenId4VpVerifierSiteDependencies"
Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipOpenId4VpVerifierBuild)' != 'true'">
<Exec WorkingDirectory="$(OpenId4VpVerifierSiteRoot)" Command="npm run build" />
<RemoveDir Directories="$(OpenId4VpVerifierWwwRoot)" />
<MakeDir Directories="$(OpenId4VpVerifierWwwRoot)" />
<ItemGroup>
<OpenId4VpVerifierDistFiles Include="$(OpenId4VpVerifierDistRoot)**/*" />
</ItemGroup>
<Copy SourceFiles="@(OpenId4VpVerifierDistFiles)"
DestinationFiles="@(OpenId4VpVerifierDistFiles->'$(OpenId4VpVerifierWwwRoot)%(RecursiveDir)%(Filename)%(Extension)')" />
<ItemGroup>
<Content Include="$(OpenId4VpVerifierWwwRoot)**/*" />
</ItemGroup>
</Target>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public AppOnboardingController(IOptions<ConsumerApiConfiguration> configuration,
[ProducesResponseType(StatusCodes.Status302Found)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[AllowAnonymous]
public IActionResult GetReference([FromRoute(Name = "referenceId")] string? _, [FromQuery] string? app)
public IActionResult GetReference([FromRoute(Name = "referenceId")] string? referenceId, [FromQuery] string? app)
{
if (_configuration == null)
return NotFound();
Expand All @@ -38,7 +38,7 @@ public IActionResult GetReference([FromRoute(Name = "referenceId")] string? _, [

var stores = ListStoresForUserAgent(selectedAppConfiguration);

return View("AppOnboarding", new AppOnboardingModel(selectedAppConfiguration, stores));
return View("AppOnboarding", new AppOnboardingModel(referenceId, selectedAppConfiguration, stores));
}

private List<AppOnboardingModel.AppStore> ListStoresForUserAgent(ConsumerApiConfiguration.AppOnboardingConfiguration.App appConfiguration)
Expand Down Expand Up @@ -110,8 +110,9 @@ public App(ConsumerApiConfiguration.AppOnboardingConfiguration.App app)

public class AppOnboardingModel
{
public AppOnboardingModel(ConsumerApiConfiguration.AppOnboardingConfiguration.App config, List<AppStore> links)
public AppOnboardingModel(string? referenceId, ConsumerApiConfiguration.AppOnboardingConfiguration.App config, List<AppStore> links)
{
ReferenceId = referenceId;
AppId = config.Id;
AppDisplayName = config.DisplayName;
AppDescription = config.Description;
Expand All @@ -121,6 +122,7 @@ public AppOnboardingModel(ConsumerApiConfiguration.AppOnboardingConfiguration.Ap
AppIconUrl = config.IconUrl;
}

public string? ReferenceId { get; }
public string AppId { get; }
public string AppDisplayName { get; }
public string AppDescription { get; set; }
Expand Down
13 changes: 12 additions & 1 deletion Applications/ConsumerApi/src/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
FROM node:24-bookworm-slim@sha256:ba849c60be29959425b8734d57b8b4b7d56f98edd9504c9af091d5281095a71e AS openid4vp-verifier-build-env

WORKDIR /src/Applications/ConsumerApi/src/OpenId4VpVerifierSite

COPY ["Applications/ConsumerApi/src/OpenId4VpVerifierSite/package.json", "Applications/ConsumerApi/src/OpenId4VpVerifierSite/package-lock.json", "./"]
RUN npm ci

COPY ["Applications/ConsumerApi/src/OpenId4VpVerifierSite/", "./"]
RUN npm run build

FROM dhi.io/dotnet:10.0.302-sdk@sha256:e18fceb745383b13f9c334a22c4c17943770129a5601033d3dfc1beff2cec17e AS build-env

ARG VERSION

WORKDIR /src

COPY . .
COPY --from=openid4vp-verifier-build-env /src/Applications/ConsumerApi/src/OpenId4VpVerifierSite/dist/openid4vp-verifier ./Applications/ConsumerApi/src/wwwroot/openid4vp-verifier

RUN dotnet restore /p:ContinuousIntegrationBuild=true "Applications/ConsumerApi/src/ConsumerApi.csproj"
RUN dotnet restore /p:ContinuousIntegrationBuild=true "Applications/HealthCheck/src/HealthCheck.csproj"
RUN dotnet publish /p:ContinuousIntegrationBuild=true --configuration Release --output /app/publish --no-restore "Applications/ConsumerApi/src/ConsumerApi.csproj"
RUN dotnet publish /p:ContinuousIntegrationBuild=true /p:SkipOpenId4VpVerifierBuild=true --configuration Release --output /app/publish --no-restore "Applications/ConsumerApi/src/ConsumerApi.csproj"
RUN dotnet publish /p:ContinuousIntegrationBuild=true --configuration Release --output /app/publish/health --no-restore "Applications/HealthCheck/src/HealthCheck.csproj"

FROM dhi.io/aspnetcore:10.0.10-debian13@sha256:36f56fe5ec5c2ac63d1451b43eb6ef00f981c9454159e3a45087a2b9071d405d
Expand Down
95 changes: 95 additions & 0 deletions Applications/ConsumerApi/src/OpenId4VpVerifierSite/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# OpenID4VP Verifier – Hinweise für AI Agents

Diese Datei gilt für den gesamten Ordner `OpenId4VpVerifierSite`. Halte sie bei jeder Änderung am Feature aktuell. Wenn sich Architektur, Validierungsregeln, unterstützte Formate, Integrationspunkte, Build-Schritte, Tests oder Designvorgaben ändern, aktualisiere diese Datei im selben Change.

## Zweck und Ablauf

Dieses Vite-/TypeScript-Bundle ergänzt die Consumer-API-Seite `/r/{referenceId}` um eine browserseitige Prüfung präsentierter Nachweise.

1. `referenceContent.ts` liest den Schlüssel aus dem URL-Fragment. Das Fragment ist base64url-kodiert und enthält `algorithm|key|forIdentity|passwordProtection`; aktuell wird nur Algorithmus `3` (`XCHACHA20_POLY1305`) unterstützt.
2. Der verschlüsselte Inhalt wird über den Token- oder RelationshipTemplate-Endpunkt der Consumer API geladen und im Browser entschlüsselt.
3. Nur Inhalte mit `@type: "TokenContentVerifiablePresentation"` werden als Präsentation behandelt. Andernfalls bleibt die reguläre Onboarding-Seite sichtbar.
4. `verification.ts` validiert `value`. Als erwartete Nonce wird die Reference-ID verwendet, als Audience derzeit fest `defaultPresentationAudience`.
5. `main.ts` verbindet Lade- und Validierungslogik mit dem vorhandenen DOM und zeigt Status sowie Credential-Inhalte an.

Das URL-Fragment wird vom Browser nicht an den Server übertragen. Verschiebe den darin enthaltenen Entschlüsselungsschlüssel nicht in Query-Parameter oder serverseitige Requests.

## Verantwortlichkeiten der Dateien

- `src/referenceContent.ts`: Referenz-Fragment parsen, API-Endpunkte auswählen, verschlüsselten Inhalt laden und entschlüsseln. Fehler führen dazu, dass kein VP-Inhalt zurückgegeben wird.
- `src/verification.ts`: Validierung und Extraktion der anzuzeigenden Credential-Daten. Der öffentliche Einstiegspunkt ist `validatePresentedCredential(presentation, options)`; Input rein, `VerificationOutcome` raus, ohne DOM-Abhängigkeit.
- `src/main.ts`: UI-Orchestrierung, DOM-Zugriff, deutsche Texte, Zuordnung von `PresentationValidationErrorCode` zu nutzerfreundlichen Meldungen und Zusammenführung optionaler `displayInformation`.
- `src/verificationKeyManagement.ts`: Browser-KMS für reine Signaturprüfung mit öffentlichen JWKs. Keine Schlüsselgenerierung, kein Import privater Schlüssel und kein Signieren.
- `src/verificationStorage.ts`: Flüchtige Storage-/Filesystem-Adapter, die Credo im Browser zum Initialisieren benötigt. Es werden keine Verifier-Daten dauerhaft gespeichert.
- `src/styles.css`: Ausschließlich Verifier-Styles; Selektoren unter `.openid4vp-verifier` beziehungsweise `#openid4vp-verifier-root` kapseln.
- `test/verification.test.ts`: Echte kryptografische Unit Tests der Validierungslogik ohne Mocks.

## Validierungsregeln

Unterstützt werden derzeit kompakte SD-JWT VCs mit Key Binding sowie JWT- und JSON-LD-basierte VCs/VPs, soweit Credo sie prüfen kann. Ein erfolgreicher Status erfordert:

- mindestens eine Präsentation beziehungsweise ein Credential;
- vorhandene erwartete Nonce und Audience;
- gültige kryptografische Signaturen;
- bei SD-JWT eine gültige Issuer-Signatur, gültige Disclosures, eine gültige Holder-Key-Binding-Signatur und einen passenden `sd_hash`;
- Übereinstimmung von Nonce und Audience mit dem aktuellen Prüfvorgang;
- einen unterstützten SD-JWT-Typ und das Pflichtfeld `vct`;
- einen bereits begonnenen und noch nicht abgelaufenen Gültigkeitszeitraum;
- bei einer VP mindestens ein eingebettetes Credential;
- bei mehreren Präsentationen beziehungsweise Credentials die Gültigkeit jedes einzelnen Elements.

Signer-Schlüssel werden aus eingebetteten JWKs, `x5c` oder unterstützten DID-URLs aufgelöst. Die Credo-Konfiguration enthält Resolver für `did:key`, `did:jwk` und `did:web`.

Wichtige bewusste Grenzen:

- Credential-Status beziehungsweise Widerruf wird aktuell nicht geprüft (`verifyCredentialStatus: false`).
- Bei `x5c` wird der öffentliche Schlüssel des Zertifikats für die Signaturprüfung verwendet; eine Zertifikatskette oder das Vertrauen in den Aussteller wird nicht validiert.
- Eine gültige Signatur allein bedeutet daher nicht, dass der Aussteller fachlich vertrauenswürdig ist.

Ändere diese Grenzen nicht beiläufig. Sicherheitsrelevante Erweiterungen benötigen passende positive und negative Tests.

## Fehlerbehandlung

Validierungsergebnisse enthalten keine Fehlermeldung als Freitext, sondern einen Wert aus `PresentationValidationErrorCode`. Ergänze für neue Fehlerfälle:

1. einen eindeutigen Enum-Wert in `verification.ts`;
2. eine korrekte Zuordnung im Validierungspfad;
3. eine kurze, nicht technische deutsche UI-Meldung in `validationErrorMessages` in `main.ts`;
4. mindestens einen Unit Test, der exakt den Error Code prüft.

Unbekannte Fehler dürfen nicht als gültig behandelt werden und werden auf `VerificationFailed` abgebildet. Interne technische Details, Bibliotheksfehler, Schlüsselmaterial und komplette Präsentationen gehören nicht in sichtbare Fehlermeldungen.

## UI- und Designvorgaben

Das Markup liegt nicht in diesem Vite-Projekt, sondern in `../Views/AppOnboarding/AppOnboarding.cshtml`. `main.ts` erwartet die dortigen `data-*`-Elemente. Ändere Markup, TypeScript-Abfragen und CSS gemeinsam, wenn dieser DOM-Vertrag angepasst wird. Erzeuge fehlende Elemente nicht dynamisch als Fallback; bei einem unvollständigen DOM soll der Verifier nicht initialisieren.

Alle sichtbaren Texte einschließlich Fehler- und Accessibility-Texte müssen auf Deutsch sein. Fehlermeldungen sollen verständlich und wenig technisch formuliert werden. Verwende `textContent`, nicht `innerHTML`, für Daten aus Präsentationen.

Alle präsentierten fachlichen Claims sollen angezeigt werden. Technische Metadaten aus `technicalClaimNames` und Bildfelder aus `imageClaimNames` werden davon bewusst ausgenommen beziehungsweise separat dargestellt. Fehlende Werte werden ausgeblendet; keine erfundenen Beispieldaten oder Anzeige-Fallbacks hinzufügen. Der Aussteller steuert zusätzlich den Bereich „verifiziert durch“. Optionale `displayInformation` aus dem Token kann Titel, Logo und Farben vorgeben.

Designquelle: [Frosch Wallet App in Figma](https://www.figma.com/design/D15DcZItr1P4lCa61vfOWN/Frosch-Wallet-App?node-id=73604-148644&m=dev). Für Designänderungen das Figma-Plugin verwenden und die Desktop-Screens direkt unterhalb des verlinkten Elements berücksichtigen; die ersten beiden dortigen Screens sind nur für die Mobile-App. Der blaue Außenrahmen in Figma stellt ein Smartphone dar und gehört nicht zum Web-UI.

## Einbettung und Build

- `vite.config.ts` erzeugt feste Namen unter `dist/openid4vp-verifier/assets/verifier.{js,css}` mit Basis-Pfad `/openid4vp-verifier/`.
- `../ConsumerApi.csproj` führt bei normalen Builds `npm ci` und `npm run build` aus und kopiert das Ergebnis nach `../wwwroot/openid4vp-verifier`.
- `../Dockerfile` baut das Bundle in einer eigenen Node-Stufe und kopiert es in das Consumer-API-Image.
- `../Views/AppOnboarding/AppOnboarding.cshtml` bindet CSS und JavaScript über `IFileVersionProvider` mit Cache-Busting ein.
- `../../../../.github/workflows/test.yml` installiert die Abhängigkeiten und führt `npm test` im Unit-Test-Job aus.

`node_modules/`, `dist/` und `../wwwroot/openid4vp-verifier/` sind generierte beziehungsweise kopierte Artefakte. Nicht direkt bearbeiten oder committen. Änderungen gehören in `src/`, das Razor-Markup oder die Build-Konfiguration. `package-lock.json` muss bei Abhängigkeitsänderungen zusammen mit `package.json` aktualisiert werden.

## Tests und lokale Prüfung

Führe im Verifier-Ordner mindestens aus:

```sh
npm ci
npm run typecheck
npm test
npm run build
```

Die Tests für `validatePresentedCredential` sollen reine Input-/Output-Tests ohne Mocks bleiben. Erzeuge signierte Test-Präsentationen mit echten Testschlüsseln und injiziere über `options.now` eine feste Zeit, damit Zeitprüfungen deterministisch sind. Decke bei Änderungen sowohl den Erfolgsfall als auch Manipulationen, falsche Bindungswerte, Zeitgrenzen, fehlende Pflichtdaten und Arrays mit teilweise ungültigen Präsentationen ab.

Wenn Markup oder Consumer-API-Einbettung geändert werden, führe zusätzlich die betroffenen .NET-Integrationstests beziehungsweise mindestens einen Build von `../ConsumerApi.csproj` aus. Bekannte Warnungen aus transitiven Kryptografie-Abhängigkeiten beim Vite-Build nicht mit Fehlern verwechseln, aber neue Warnungen prüfen und dokumentieren.
29 changes: 29 additions & 0 deletions Applications/ConsumerApi/src/OpenId4VpVerifierSite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# OpenID4VP Verifier Site

This Vite application builds the static OpenID4VP verifier bundle that is embedded into the Consumer API `/r/{referenceId}` onboarding page.

## Build

```sh
npm install
npm run build
```

The Vite build output is written to `dist/openid4vp-verifier` and exposes fixed asset names under `/openid4vp-verifier/assets/`.

During the Consumer API build, MSBuild runs the Vite build and copies the generated files into `../wwwroot/openid4vp-verifier`. The copied `wwwroot` files are build artifacts and are not committed.

## Input

The bundle reads the NMSHD reference fragment from the current `/r/{referenceId}` URL. The fragment is base64url encoded and contains `algorithm|key|forIdentity|passwordProtection`. For this first version, only algorithm `3` (`XCHACHA20_POLY1305`) is supported.

The referenced Token or RelationshipTemplate content is fetched from the Consumer API. If the decrypted JSON has `@type: "TokenContentVerifiablePresentation"`, its `value` is verified as a presented credential using the reference id as expected nonce and `defaultPresentationAudience` as expected audience. Otherwise, the original onboarding page is shown.

Credential display values are extracted from the Verifiable Presentation. Missing values are omitted instead of being replaced with sample/default credential data.

Optional validation parameters:

- `nonce` or `expected_nonce`: expected presentation challenge.
- `audience` or `client_id`: expected presentation audience. If omitted, the current origin is used.

The implementation performs a minimal browser-side validation using `@credo-ts/core` and `@credo-ts/openid4vc` types. NMSHD token content is decrypted with `@nmshd/crypto`. Credential status checks are disabled for this first version.
Loading