Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -86,6 +86,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
96 changes: 96 additions & 0 deletions Applications/ConsumerApi/src/OpenId4VpVerifierSite/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# OpenID4VP Verifier – Instructions for AI Agents

This file applies to the entire `OpenId4VpVerifierSite` directory. Keep it up to date with every change to the feature. If the architecture, validation rules, supported formats, integration points, build steps, tests, or design requirements change, update this file as part of the same change.

## Purpose and Flow

This Vite/TypeScript bundle adds browser-side verification of presented credentials to the Consumer API page `/r/{referenceId}`.

1. `referenceContent.ts` reads the key from the URL fragment. The fragment is base64url-encoded and contains `algorithm|key|forIdentity|passwordProtection`; currently, only algorithm `3` (`XCHACHA20_POLY1305`) is supported.
2. The encrypted content is loaded through the Consumer API's token or RelationshipTemplate endpoint and decrypted in the browser.
3. Only content with `@type: "TokenContentVerifiablePresentation"` is treated as a presentation. Otherwise, the regular onboarding page remains visible.
4. `verification.ts` validates `value`. The reference ID is used as the expected nonce, while the audience is currently hard-coded to `defaultPresentationAudience`.
5. `main.ts` connects the loading and validation logic to the existing DOM and displays the status and credential contents.

The URL fragment is not sent to the server by the browser. Do not move the decryption key it contains into query parameters or server-side requests.

## File Responsibilities

- `src/referenceContent.ts`: Parse the reference fragment, select API endpoints, and load and decrypt the encrypted content. Errors result in no VP content being returned.
- `src/verification.ts`: Validate and extract the credential data to be displayed. The public entry point is `validatePresentedCredential(presentation, options)`; input in, `VerificationOutcome` out, with no DOM dependency.
- `src/main.ts`: UI orchestration, DOM access, German text, mapping `PresentationValidationErrorCode` values to user-friendly messages, and merging optional `displayInformation`.
- `src/verificationKeyManagement.ts`: Browser KMS exclusively for signature verification with public JWKs. No key generation, private-key import, or signing.
- `src/verificationStorage.ts`: Ephemeral storage/filesystem adapters required to initialize Credo in the browser. No verifier data is stored persistently.
- `src/styles.css`: Verifier styles only; scope selectors beneath `.openid4vp-verifier` or `#openid4vp-verifier-root`.
- `test/verification.test.ts`: Real cryptographic unit tests of the validation logic without mocks.

## Validation Rules

Compact SD-JWT VCs with Key Binding as well as JWT- and JSON-LD-based VCs/VPs are currently supported to the extent that Credo can verify them. A successful status requires:

- at least one presentation or credential;
- the expected nonce and audience to be present;
- valid cryptographic signatures;
- JWT and JSON-LD credentials to be embedded in a request-bound Verifiable Presentation rather than supplied as standalone credentials;
- for SD-JWT, a valid issuer signature, valid disclosures, a valid holder key-binding signature, and a matching `sd_hash`;
- the nonce and audience to match the current verification process;
- a supported SD-JWT type and the required `vct` field;
- a validity period that has already started and has not yet expired;
- at least one embedded credential in a VP;
- every individual item to be valid when multiple presentations or credentials are supplied.

Signer keys are resolved from embedded JWKs, `x5c`, or supported DID URLs. The Credo configuration includes resolvers for `did:key`, `did:jwk`, and `did:web`.

Important intentional limitations:

- Credential status or revocation is not currently checked (`verifyCredentialStatus: false`).
- With `x5c`, the certificate's public key is used to verify the signature; neither the certificate chain nor trust in the issuer is validated.
- A valid signature alone therefore does not mean that the issuer is trusted from a business perspective.

Do not change these limitations casually. Security-relevant extensions require appropriate positive and negative tests.

## Error Handling

Validation results do not contain a free-text error message, but a value from `PresentationValidationErrorCode`. For new error cases, add:

1. a distinct enum value in `verification.ts`;
2. the correct mapping in the validation path;
3. a short, non-technical German UI message in `validationErrorMessages` in `main.ts`;
4. at least one unit test that asserts the exact error code.

Unknown errors must not be treated as valid and are mapped to `VerificationFailed`. Internal technical details, library errors, key material, and complete presentations must not appear in visible error messages.

## UI and Design Requirements

The markup is not located in this Vite project, but in `../Views/AppOnboarding/AppOnboarding.cshtml`. `main.ts` expects the `data-*` elements defined there. Change the markup, TypeScript queries, and CSS together when modifying this DOM contract. Do not create missing elements dynamically as a fallback; the verifier must not initialize when the DOM is incomplete.

All visible text, including error and accessibility text, must be in German. Error messages should be understandable and use minimal technical language. Use `textContent`, not `innerHTML`, for data from presentations.

All presented domain claims should be displayed. Technical metadata listed in `technicalClaimNames` and image fields listed in `imageClaimNames` are intentionally excluded or displayed separately. Hide missing values; do not add invented sample data or display fallbacks. The issuer also controls the “verifiziert durch” section. Optional `displayInformation` from the token can specify the title, logo, and colors.

Design source: [Frosch Wallet App in Figma](https://www.figma.com/design/D15DcZItr1P4lCa61vfOWN/Frosch-Wallet-App?node-id=73604-148644&m=dev). For design changes, use the Figma plugin and consider the desktop screens directly below the linked element; the first two screens there are only for the mobile app. The blue outer frame in Figma represents a smartphone and is not part of the web UI.

## Embedding and Build

- `vite.config.ts` generates fixed filenames under `dist/openid4vp-verifier/assets/verifier.{js,css}` with the base path `/openid4vp-verifier/`.
- `../ConsumerApi.csproj` runs `npm ci` and `npm run build` during regular builds and copies the result to `../wwwroot/openid4vp-verifier`.
- `../Dockerfile` builds the bundle in a dedicated Node stage and copies it into the Consumer API image.
- `../Views/AppOnboarding/AppOnboarding.cshtml` includes CSS and preloads and dynamically imports JavaScript through `IFileVersionProvider` with cache busting. The server-rendered verifier remains visible during normal loading, while import or initialization failures restore the onboarding page.
- `../../../../.github/workflows/test.yml` installs the dependencies and runs `npm test` in the unit-test job.

`node_modules/`, `dist/`, and `../wwwroot/openid4vp-verifier/` are generated or copied artifacts. Do not edit or commit them directly. Changes belong in `src/`, the Razor markup, or the build configuration. When dependencies change, `package-lock.json` must be updated together with `package.json`.

## Tests and Local Verification

Run at least the following commands in the verifier directory:

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

Tests for `validatePresentedCredential` should remain pure input/output tests without mocks. Generate signed test presentations with real test keys and inject a fixed time through `options.now` so that time checks are deterministic. When making changes, cover the success case as well as tampering, incorrect binding values, time boundaries, missing required data, and arrays containing partially invalid presentations.

When changing the markup or Consumer API embedding, also run the affected .NET integration tests or at least build `../ConsumerApi.csproj`. Do not confuse known warnings from transitive cryptography dependencies during the Vite build with errors, but investigate and document new warnings.
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