Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4b27b9b
feat: verify presentation token
Magnus-Kuhn Mar 19, 2026
250237f
fix: allow empty tokens without password
Magnus-Kuhn Mar 20, 2026
e715acf
Merge branch 'release/openid4vc' into verify-presentation
Magnus-Kuhn Mar 20, 2026
d4b4f58
test: adapt app string processor test
Magnus-Kuhn Mar 20, 2026
25e1098
test: remove happy path isolation test
Magnus-Kuhn Mar 20, 2026
537ba85
refactor: adapt to optional password protection of empty token
Magnus-Kuhn Mar 20, 2026
db2b579
refactor: export verify use case
Magnus-Kuhn Mar 20, 2026
ff3a208
test: cleaner file copying into container
Magnus-Kuhn Mar 20, 2026
45fc3cb
feat: improve unsupported VC type error message
Magnus-Kuhn Mar 20, 2026
6a61c3a
test: add negative tests
Magnus-Kuhn Mar 20, 2026
5dcc7e3
refactor: don't return classes in UseCases
Magnus-Kuhn Mar 20, 2026
eedcb09
test: re-add test skip
Magnus-Kuhn Mar 20, 2026
fb57fc7
chore: build schemas
Magnus-Kuhn Mar 20, 2026
09d9819
test: null checks
Magnus-Kuhn Mar 20, 2026
ddd726c
test: split presentation token test
Magnus-Kuhn Mar 23, 2026
bb7a885
test: move startEudiplo into .dev
Magnus-Kuhn Mar 23, 2026
f5b555c
test: fix the split
Magnus-Kuhn Mar 23, 2026
e17e518
test: remove now obsolete startEudiplo function
Magnus-Kuhn Mar 23, 2026
6c44bdd
Merge branch 'release/openid4vc' into verify-presentation
Magnus-Kuhn Mar 26, 2026
760543f
fix: remove password protection from sendEmptyTokenParameters
Magnus-Kuhn Mar 26, 2026
355c488
fix: undo credential lifetime increase
Magnus-Kuhn Mar 26, 2026
f8441bf
test: make tests independent of each other
tnotheis Mar 27, 2026
f47ac35
test: improve startOid4VcComposeStack helper method
tnotheis Mar 27, 2026
c535f42
chore: npm audit fix
tnotheis Mar 27, 2026
3a712d0
chore: ignore vulnerability
tnotheis Mar 27, 2026
f75d8f1
test: rename eudiplo tenant
Magnus-Kuhn Mar 27, 2026
1b66718
test: namings in app string processor test
Magnus-Kuhn Mar 27, 2026
f1b73a5
feat: simplify use case interface
Magnus-Kuhn Mar 27, 2026
b9c59f6
test: set correct NMSHD_TEST_BASEURL in oid4vc compose stack
tnotheis Mar 27, 2026
9a9ed4e
Merge branch 'verify-presentation' of https://github.com/nmshd/runtim…
Magnus-Kuhn Mar 27, 2026
9b618b9
fix: remove stack from error message
Magnus-Kuhn Mar 27, 2026
c8acdb3
test: adapt runtime tests
Magnus-Kuhn Mar 27, 2026
d02508d
test: re-add invalid nonce test
Magnus-Kuhn Mar 27, 2026
721f7c7
test: use new helper functions more
Magnus-Kuhn Mar 27, 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
2 changes: 1 addition & 1 deletion .ci/runChecks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ npm run lint:eslint
npm run lint:prettier
npm run --workspaces cdep
npx --workspaces license-check
npx better-npm-audit audit --exclude 1112030
npx better-npm-audit audit --exclude 1112030,1115432
2 changes: 1 addition & 1 deletion .dev/compose.openid4vc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ services:
ports:
- "3000:3000"
volumes:
- ./eudiplo-assets:/app/assets/config
- ./eudiplo/config:/app/assets/config

networks:
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"name": "test",
"name": "test-tenant",
"description": "test tenant"
}
25 changes: 25 additions & 0 deletions .dev/eudiplo/startEudiplo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import path from "path";
import { GenericContainer, StartedTestContainer, Wait } from "testcontainers";

export async function startEudiplo(): Promise<StartedTestContainer> {
return await new GenericContainer("ghcr.io/openwallet-foundation-labs/eudiplo:3.1.2@sha256:0ea3a73d42a1eb10a6edc45e3289478b08b09064bd75563c503ed12be2ed2dc6")
.withEnvironment({
PUBLIC_URL: "http://localhost:3000", // eslint-disable-line @typescript-eslint/naming-convention
MASTER_SECRET: "OgwrDcgVQQ2yZwcFt7kPxQm3nUF+X3etF6MdLTstZAY=", // eslint-disable-line @typescript-eslint/naming-convention
AUTH_CLIENT_ID: "root", // eslint-disable-line @typescript-eslint/naming-convention
AUTH_CLIENT_SECRET: "test", // eslint-disable-line @typescript-eslint/naming-convention
CONFIG_IMPORT: "true", // eslint-disable-line @typescript-eslint/naming-convention
CONFIG_FOLDER: "/app/assets/config", // eslint-disable-line @typescript-eslint/naming-convention
PORT: "3000" // eslint-disable-line @typescript-eslint/naming-convention
} as Record<string, string>)
.withExposedPorts({ container: 3000, host: 3000 })
.withCopyDirectoriesToContainer([
{
source: path.resolve(path.join(__dirname, "config")),
target: "/app/assets/config"
}
])
.withStartupTimeout(60000)
.withWaitStrategy(Wait.forHealthCheck())
.start();
}
163 changes: 109 additions & 54 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/app-runtime/src/AppStringProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,12 @@ export class AppStringProcessor {
// RelationshipTemplates are processed by the RequestModule
break;
case "Token":
const tokenContent = this.parseTokenContent(result.value.value.content);
const token = result.value.value;
const tokenContent = this.parseTokenContent(token.content);

if (tokenContent instanceof TokenContentVerifiablePresentation) {
// TODO: add technical validation
await uiBridge.showVerifiablePresentation(account, result.value.value, true);
const verificationResult = await services.consumptionServices.openId4Vc.verifyPresentationToken({ token });
await uiBridge.showVerifiablePresentation(account, result.value.value, verificationResult.value.isValid);
break;
}
return Result.fail(AppRuntimeErrors.appStringProcessor.notSupportedTokenContent());
Expand Down
47 changes: 40 additions & 7 deletions packages/app-runtime/test/runtime/AppStringProcessor.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ArbitraryRelationshipTemplateContentJSON, AuthenticationRequestItem, RelationshipTemplateContent, TokenContentVerifiablePresentation } from "@nmshd/content";
import { EudiploClient } from "@eudiplo/sdk-core";
import { ArbitraryRelationshipTemplateContentJSON, AuthenticationRequestItem, RelationshipTemplateContent } from "@nmshd/content";
import { CoreDate, PasswordLocationIndicatorOptions } from "@nmshd/core-types";
import { DeviceOnboardingInfoDTO, PeerRelationshipTemplateLoadedEvent } from "@nmshd/runtime";
import assert from "assert";
import { startEudiplo } from "../../../../.dev/eudiplo/startEudiplo";
import { AppRuntime, LocalAccountSession } from "../../src";
import { MockEventBus, MockUIBridge, TestUtil } from "../lib";

Expand Down Expand Up @@ -378,21 +380,52 @@ describe("AppStringProcessor", function () {
});

test("get a token with verifiable presentation content using a url", async function () {
const tokenResult = await runtime1Session.transportServices.tokens.createOwnToken({
content: TokenContentVerifiablePresentation.from({
value: { claim: "test" },
type: "dc+sd-jwt"
}).toJSON(),
const eudiploClientId = "test-admin";
const eudiploClientSecret = "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5";
const eudiploCredentialConfigurationId = "test";
const eudiploBaseUrl = "http://localhost:3000";

const eudiploContainer = await startEudiplo();

const eudiploClient = new EudiploClient({
baseUrl: eudiploBaseUrl,
clientId: eudiploClientId,
clientSecret: eudiploClientSecret
});

const credentialOfferUrl = (
await eudiploClient.createIssuanceOffer({
responseType: "uri",
credentialConfigurationIds: [eudiploCredentialConfigurationId],
flow: "pre_authorized_code"
})
).uri;

const resolveCredentialOfferResult = await runtime1Session.consumptionServices.openId4Vc.resolveCredentialOffer({ credentialOfferUrl });
const requestCredentialsResult = await runtime1Session.consumptionServices.openId4Vc.requestCredentials({
credentialOffer: resolveCredentialOfferResult.value.credentialOffer,
credentialConfigurationIds: [eudiploCredentialConfigurationId]
});
const storedCredential = (
await runtime1Session.consumptionServices.openId4Vc.storeCredentials({
credentialResponses: requestCredentialsResult.value.credentialResponses
})
).value;

const createPresentationTokenResult = await runtime1Session.consumptionServices.openId4Vc.createPresentationToken({
attributeId: storedCredential.id,
expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(),
ephemeral: true
});
const token = tokenResult.value;
const token = createPresentationTokenResult.value;

const result = await runtime4.stringProcessor.processURL(token.reference.url, runtime4Session.account);
expect(result).toBeSuccessful();
expect(result.value).toBeUndefined();

expect(runtime4MockUiBridge).showVerifiablePresentationCalled(token.id, true);

await eudiploContainer.stop();
});

test("get a template using a url", async function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,8 @@ export class OpenId4VcController extends ConsumptionBaseController {
public async createPresentationTokenContent(credential: VerifiableCredential, nonce: string): Promise<TokenContentVerifiablePresentation> {
return await this.holder.createPresentationTokenContent(credential, nonce);
}

public async verifyPresentationTokenContent(tokenContent: TokenContentVerifiablePresentation, expectedNonce: string): Promise<{ isValid: boolean; error?: Error }> {
return await this.holder.verifyPresentationTokenContent(tokenContent, expectedNonce);
}
}
17 changes: 16 additions & 1 deletion packages/consumption/src/modules/openid4vc/local/Holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
// hacky solution because credo doesn't support credentials without key binding
// TODO: use credentials without key binding once supported
public async createPresentationTokenContent(credential: VerifiableCredential, nonce: string): Promise<TokenContentVerifiablePresentation> {
if (credential.type !== ClaimFormat.SdJwtDc) throw new Error("Only SD-JWT credentials have been tested so far with token presentation");
if (credential.type !== ClaimFormat.SdJwtDc) throw new Error("Only SD-JWT credentials are supported for token presentation");

const sdJwtVcApi = this.agent.dependencyManager.resolve(SdJwtVcApi);
const presentation = await sdJwtVcApi.present({
Expand All @@ -224,6 +224,21 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
});
}

public async verifyPresentationTokenContent(tokenContent: TokenContentVerifiablePresentation, expectedNonce: string): Promise<{ isValid: boolean; error?: Error }> {
if (tokenContent.type !== ClaimFormat.SdJwtDc) throw new Error("Only SD-JWT credentials are supported for token presentation");

const sdJwtVcApi = this.agent.dependencyManager.resolve(SdJwtVcApi);
const verificationResult = await sdJwtVcApi.verify({
compactSdJwtVc: tokenContent.value as string,
keyBinding: {
audience: "defaultPresentationAudience",
nonce: expectedNonce
}
});

return { isValid: verificationResult.isValid, error: "error" in verificationResult ? verificationResult.error : undefined };
}

public async exit(): Promise<void> {
await this.shutdown();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
ResolveCredentialOfferResponse,
ResolveCredentialOfferUseCase,
StoreCredentialsRequest,
StoreCredentialsUseCase
StoreCredentialsUseCase,
VerifyPresentationTokenRequest,
VerifyPresentationTokenResponse,
VerifyPresentationTokenUseCase
} from "../../../useCases";

export class OpenId4VcFacade {
Expand All @@ -27,7 +30,8 @@ export class OpenId4VcFacade {
@Inject private readonly storeCredentialsUseCase: StoreCredentialsUseCase,
@Inject private readonly resolveAuthorizationRequestUseCase: ResolveAuthorizationRequestUseCase,
@Inject private readonly acceptAuthorizationRequestUseCase: AcceptAuthorizationRequestUseCase,
@Inject private readonly createPresentationTokenUseCase: CreatePresentationTokenUseCase
@Inject private readonly createPresentationTokenUseCase: CreatePresentationTokenUseCase,
@Inject private readonly verifyPresentationTokenUseCase: VerifyPresentationTokenUseCase
) {}

public async resolveCredentialOffer(request: ResolveCredentialOfferRequest): Promise<Result<ResolveCredentialOfferResponse>> {
Expand All @@ -53,4 +57,8 @@ export class OpenId4VcFacade {
public async createPresentationToken(request: CreatePresentationTokenRequest): Promise<Result<TokenDTO>> {
return await this.createPresentationTokenUseCase.execute(request);
}

public async verifyPresentationToken(request: VerifyPresentationTokenRequest): Promise<Result<VerifyPresentationTokenResponse>> {
return await this.verifyPresentationTokenUseCase.execute(request);
}
}
170 changes: 170 additions & 0 deletions packages/runtime/src/useCases/common/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17507,6 +17507,176 @@ export const StoreCredentialsRequest: any = {
}
}

export const VerifyPresentationTokenRequest: any = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/VerifyPresentationTokenRequest",
"definitions": {
"VerifyPresentationTokenRequest": {
"type": "object",
"properties": {
"token": {
"type": "object",
"additionalProperties": false,
"properties": {
"content": {
"$ref": "#/definitions/TokenContentVerifiablePresentationJSON"
},
"id": {
"type": "string"
},
"isOwn": {
"type": "boolean"
},
"createdBy": {
"type": "string"
},
"createdByDevice": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"expiresAt": {
"type": "string"
},
"forIdentity": {
"type": "string"
},
"passwordProtection": {
"$ref": "#/definitions/PasswordProtectionDTO"
},
"reference": {
"type": "object",
"properties": {
"truncated": {
"type": "string"
},
"url": {
"type": "string"
}
},
"required": [
"truncated",
"url"
],
"additionalProperties": false
},
"isEphemeral": {
"type": "boolean"
}
},
"required": [
"content",
"createdAt",
"createdBy",
"createdByDevice",
"expiresAt",
"id",
"isEphemeral",
"isOwn",
"reference"
]
}
},
"required": [
"token"
],
"additionalProperties": false
},
"TokenContentVerifiablePresentationJSON": {
"type": "object",
"properties": {
"@type": {
"type": "string",
"const": "TokenContentVerifiablePresentation"
},
"@context": {
"type": "string"
},
"@version": {
"type": "string"
},
"value": {
"anyOf": [
{
"type": "string"
},
{
"type": "object"
}
]
},
"type": {
"type": "string"
},
"displayInformation": {
"type": "array",
"items": {
"type": "object"
}
}
},
"required": [
"@type",
"type",
"value"
],
"additionalProperties": false
},
"PasswordProtectionDTO": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"passwordIsPin": {
"type": "boolean",
"const": true
},
"passwordLocationIndicator": {
"anyOf": [
{
"type": "string",
"const": "RecoveryKit"
},
{
"type": "string",
"const": "Self"
},
{
"type": "string",
"const": "Letter"
},
{
"type": "string",
"const": "RegistrationLetter"
},
{
"type": "string",
"const": "Email"
},
{
"type": "string",
"const": "SMS"
},
{
"type": "string",
"const": "Website"
},
{
"type": "number"
}
]
}
},
"required": [
"password"
],
"additionalProperties": false
}
}
}

export const CreateSettingRequest: any = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/CreateSettingRequest",
Expand Down
Loading
Loading