diff --git a/.ci/runChecks.sh b/.ci/runChecks.sh index f94dfef62..3422cf91e 100755 --- a/.ci/runChecks.sh +++ b/.ci/runChecks.sh @@ -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 diff --git a/.dev/compose.openid4vc.yml b/.dev/compose.openid4vc.yml index d01184ea3..c4cf0ad0d 100644 --- a/.dev/compose.openid4vc.yml +++ b/.dev/compose.openid4vc.yml @@ -60,7 +60,7 @@ services: ports: - "3000:3000" volumes: - - ./eudiplo-assets:/app/assets/config + - ./eudiplo/config:/app/assets/config networks: default: diff --git a/.dev/eudiplo-assets/test/certs/certificate.json b/.dev/eudiplo/config/test-tenant/certs/certificate.json similarity index 100% rename from .dev/eudiplo-assets/test/certs/certificate.json rename to .dev/eudiplo/config/test-tenant/certs/certificate.json diff --git a/.dev/eudiplo-assets/test/clients/test-admin.json b/.dev/eudiplo/config/test-tenant/clients/test-admin.json similarity index 100% rename from .dev/eudiplo-assets/test/clients/test-admin.json rename to .dev/eudiplo/config/test-tenant/clients/test-admin.json diff --git a/.dev/eudiplo-assets/test/images/logo.png b/.dev/eudiplo/config/test-tenant/images/logo.png similarity index 100% rename from .dev/eudiplo-assets/test/images/logo.png rename to .dev/eudiplo/config/test-tenant/images/logo.png diff --git a/.dev/eudiplo-assets/test/info.json b/.dev/eudiplo/config/test-tenant/info.json similarity index 57% rename from .dev/eudiplo-assets/test/info.json rename to .dev/eudiplo/config/test-tenant/info.json index 22e908c4a..3b1aa63ae 100644 --- a/.dev/eudiplo-assets/test/info.json +++ b/.dev/eudiplo/config/test-tenant/info.json @@ -1,4 +1,4 @@ { - "name": "test", + "name": "test-tenant", "description": "test tenant" } diff --git a/.dev/eudiplo-assets/test/issuance/credentials/test.json b/.dev/eudiplo/config/test-tenant/issuance/credentials/test.json similarity index 100% rename from .dev/eudiplo-assets/test/issuance/credentials/test.json rename to .dev/eudiplo/config/test-tenant/issuance/credentials/test.json diff --git a/.dev/eudiplo-assets/test/issuance/issuance.json b/.dev/eudiplo/config/test-tenant/issuance/issuance.json similarity index 100% rename from .dev/eudiplo-assets/test/issuance/issuance.json rename to .dev/eudiplo/config/test-tenant/issuance/issuance.json diff --git a/.dev/eudiplo-assets/test/keys/key.json b/.dev/eudiplo/config/test-tenant/keys/key.json similarity index 100% rename from .dev/eudiplo-assets/test/keys/key.json rename to .dev/eudiplo/config/test-tenant/keys/key.json diff --git a/.dev/eudiplo-assets/test/presentation/test.json b/.dev/eudiplo/config/test-tenant/presentation/test.json similarity index 100% rename from .dev/eudiplo-assets/test/presentation/test.json rename to .dev/eudiplo/config/test-tenant/presentation/test.json diff --git a/.dev/eudiplo/startEudiplo.ts b/.dev/eudiplo/startEudiplo.ts new file mode 100644 index 000000000..115341902 --- /dev/null +++ b/.dev/eudiplo/startEudiplo.ts @@ -0,0 +1,25 @@ +import path from "path"; +import { GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +export async function startEudiplo(): Promise { + 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) + .withExposedPorts({ container: 3000, host: 3000 }) + .withCopyDirectoriesToContainer([ + { + source: path.resolve(path.join(__dirname, "config")), + target: "/app/assets/config" + } + ]) + .withStartupTimeout(60000) + .withWaitStrategy(Wait.forHealthCheck()) + .start(); +} diff --git a/package-lock.json b/package-lock.json index 9fade5242..459015059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3280,7 +3280,9 @@ } }, "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4117,7 +4119,9 @@ } }, "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4435,15 +4439,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -4456,7 +4462,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -4470,14 +4476,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -4493,12 +4501,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -4513,12 +4523,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4529,7 +4541,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -4544,13 +4558,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -4567,7 +4583,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -4579,16 +4597,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -4604,23 +4624,40 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4638,14 +4675,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4660,11 +4699,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5869,7 +5910,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7902,7 +7945,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -7953,7 +7998,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8183,7 +8230,9 @@ } }, "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11388,7 +11437,9 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -12835,7 +12886,9 @@ } }, "node_modules/ts-json-schema-generator/node_modules/brace-expansion": { - "version": "5.0.4", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13093,14 +13146,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 1f494ae75..589f842e1 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -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()); diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 8396618dd..aecfc06b3 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -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"; @@ -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 () { diff --git a/packages/consumption/src/modules/openid4vc/OpenId4VcController.ts b/packages/consumption/src/modules/openid4vc/OpenId4VcController.ts index 9d59f4e36..c7c6f11fa 100644 --- a/packages/consumption/src/modules/openid4vc/OpenId4VcController.ts +++ b/packages/consumption/src/modules/openid4vc/OpenId4VcController.ts @@ -133,4 +133,8 @@ export class OpenId4VcController extends ConsumptionBaseController { public async createPresentationTokenContent(credential: VerifiableCredential, nonce: string): Promise { 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); + } } diff --git a/packages/consumption/src/modules/openid4vc/local/Holder.ts b/packages/consumption/src/modules/openid4vc/local/Holder.ts index f3b4ea48d..701edd701 100644 --- a/packages/consumption/src/modules/openid4vc/local/Holder.ts +++ b/packages/consumption/src/modules/openid4vc/local/Holder.ts @@ -205,7 +205,7 @@ export class Holder extends BaseAgent> // 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 { - 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({ @@ -224,6 +224,21 @@ export class Holder extends BaseAgent> }); } + 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 { await this.shutdown(); } diff --git a/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts b/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts index 00b65dc75..ad16dbcfb 100644 --- a/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts +++ b/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts @@ -17,7 +17,10 @@ import { ResolveCredentialOfferResponse, ResolveCredentialOfferUseCase, StoreCredentialsRequest, - StoreCredentialsUseCase + StoreCredentialsUseCase, + VerifyPresentationTokenRequest, + VerifyPresentationTokenResponse, + VerifyPresentationTokenUseCase } from "../../../useCases"; export class OpenId4VcFacade { @@ -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> { @@ -53,4 +57,8 @@ export class OpenId4VcFacade { public async createPresentationToken(request: CreatePresentationTokenRequest): Promise> { return await this.createPresentationTokenUseCase.execute(request); } + + public async verifyPresentationToken(request: VerifyPresentationTokenRequest): Promise> { + return await this.verifyPresentationTokenUseCase.execute(request); + } } diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 1c8a4a434..5605b2943 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -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", diff --git a/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts new file mode 100644 index 000000000..82119b55f --- /dev/null +++ b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts @@ -0,0 +1,42 @@ +import { Result } from "@js-soft/ts-utils"; +import { OpenId4VcController } from "@nmshd/consumption"; +import { TokenContentVerifiablePresentation, TokenContentVerifiablePresentationJSON } from "@nmshd/content"; +import { TokenDTO } from "@nmshd/runtime-types"; +import { Inject } from "@nmshd/typescript-ioc"; +import { SchemaRepository, SchemaValidator, UseCase } from "../../common"; + +export interface VerifyPresentationTokenRequest { + token: Omit & { content: TokenContentVerifiablePresentationJSON }; +} + +export interface VerifyPresentationTokenResponse { + isValid: boolean; + error?: { + name: string; + message: string; + }; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("VerifyPresentationTokenRequest")); + } +} + +export class VerifyPresentationTokenUseCase extends UseCase { + public constructor( + @Inject private readonly openId4VcController: OpenId4VcController, + @Inject validator: Validator + ) { + super(validator); + } + + protected override async executeInternal(request: VerifyPresentationTokenRequest): Promise> { + const verificationResult = await this.openId4VcController.verifyPresentationTokenContent(TokenContentVerifiablePresentation.from(request.token.content), request.token.id); + + return Result.ok({ + isValid: verificationResult.isValid, + error: verificationResult.error ? { name: verificationResult.error.name, message: verificationResult.error.message } : undefined + }); + } +} diff --git a/packages/runtime/src/useCases/consumption/openid4vc/index.ts b/packages/runtime/src/useCases/consumption/openid4vc/index.ts index 024905d25..f7d23b806 100644 --- a/packages/runtime/src/useCases/consumption/openid4vc/index.ts +++ b/packages/runtime/src/useCases/consumption/openid4vc/index.ts @@ -4,3 +4,4 @@ export * from "./RequestCredentials"; export * from "./ResolveAuthorizationRequest"; export * from "./ResolveCredentialOffer"; export * from "./StoreCredentials"; +export * from "./VerifyPresentationToken"; diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 2a9614fe8..ab011828d 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -1,14 +1,14 @@ import { SdJwtVcRecord } from "@credo-ts/core"; import { EudiploClient } from "@eudiplo/sdk-core"; import { AcceptProposeAttributeRequestItemParametersWithNewAttributeJSON, AcceptShareAuthorizationRequestRequestItemParametersJSON, decodeRecord } from "@nmshd/consumption"; -import { RequestJSON, ShareAuthorizationRequestRequestItemJSON, TokenContentVerifiablePresentation, VerifiableCredentialJSON } from "@nmshd/content"; +import { RequestJSON, ShareAuthorizationRequestRequestItemJSON, TokenContentVerifiablePresentationJSON, VerifiableCredentialJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import axios, { AxiosInstance } from "axios"; import * as client from "openid-client"; import path from "path"; import { DockerComposeEnvironment, StartedDockerComposeEnvironment, Wait } from "testcontainers"; import { Agent as UndiciAgent, fetch as undiciFetch } from "undici"; -import { IncomingRequestStatusChangedEvent } from "../../src"; +import { IncomingRequestStatusChangedEvent, LocalAttributeDTO } from "../../src"; import { RuntimeServiceProvider, syncUntilHasMessageWithRequest, syncUntilHasRelationships, TestRuntimeServices } from "../lib"; const fetchInstance: typeof fetch = (async (input: any, init: any) => { @@ -16,6 +16,14 @@ const fetchInstance: typeof fetch = (async (input: any, init: any) => { return response; }) as unknown as typeof fetch; +const eudiploClientId = "test-admin"; +const eudiploClientSecret = "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5"; + +const eudiploPresentationConfigurationId = "test"; +const eudiploCredentialConfigurationId = "test"; + +let eudiploClient: EudiploClient; + const runtimeServiceProvider = new RuntimeServiceProvider(fetchInstance); let runtimeServices1: TestRuntimeServices; @@ -41,6 +49,14 @@ beforeAll(async () => { } }); await createActiveRelationshipToService(runtimeServices1, serviceAxiosInstance); + + const eudiploBaseUrl = "http://localhost:3000"; + + eudiploClient = new EudiploClient({ + baseUrl: eudiploBaseUrl, + clientId: eudiploClientId, + clientSecret: eudiploClientSecret + }); }, 120000); afterAll(async () => { @@ -49,117 +65,149 @@ afterAll(async () => { if (dockerComposeStack) await dockerComposeStack.down(); }); -describe("EUDIPLO", () => { - const clientId = "test-admin"; - const clientSecret = "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5"; +test("issuance", async () => { + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code" + }) + ).uri; + + const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); + expect(resolveCredentialOfferResult).toBeSuccessful(); - const eudiploPresentationConfigurationId = "test"; - const eudiploCredentialConfigurationId = "test"; + const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer: resolveCredentialOfferResult.value.credentialOffer, + credentialConfigurationIds: [eudiploCredentialConfigurationId] + }); + const storeCredentialsResponse = await runtimeServices1.consumption.openId4Vc.storeCredentials({ + credentialResponses: credentialResponsesResult.value.credentialResponses + }); + expect(storeCredentialsResponse).toBeSuccessful(); + expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].logo).toBeDefined(); + expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); +}); - let eudiploClient: EudiploClient; +test("issuance with pin authentication", async () => { + const pin = "1234"; - beforeAll(() => { - const baseUrl = `http://localhost:3000`; + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code", + txCode: pin + }) + ).uri; - eudiploClient = new EudiploClient({ - baseUrl, - clientId, - clientSecret - }); + const result = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ + credentialOfferUrl }); - test("issuance", async () => { - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "pre_authorized_code" - }) - ).uri; + expect(result).toBeSuccessful(); - const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); - expect(resolveCredentialOfferResult).toBeSuccessful(); + const credentialOffer = result.value.credentialOffer; + const requestedCredentials = credentialOffer.credentialOfferPayload.credential_configuration_ids; - const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer: resolveCredentialOfferResult.value.credentialOffer, - credentialConfigurationIds: [eudiploCredentialConfigurationId] - }); - const storeCredentialsResponse = await runtimeServices1.consumption.openId4Vc.storeCredentials({ - credentialResponses: credentialResponsesResult.value.credentialResponses - }); - expect(storeCredentialsResponse).toBeSuccessful(); - expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].logo).toBeDefined(); - expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); + const wrongPinRequestResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer, + credentialConfigurationIds: requestedCredentials, + pinCode: `1${pin}` }); + expect(wrongPinRequestResult.isError).toBe(true); - test("issuance with pin authentication", async () => { - const pin = "1234"; - - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "pre_authorized_code", - txCode: pin - }) - ).uri; + const requestResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer, + credentialConfigurationIds: requestedCredentials, + pinCode: pin + }); + expect(requestResult).toBeSuccessful(); +}); - const result = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ - credentialOfferUrl - }); +// external authentication buggy in the latest release (1.16.0) +// eslint-disable-next-line jest/no-disabled-tests +test.skip("issuance with external authentication", async () => { + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "authorization_code" + }) + ).uri; + + const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); + expect(resolveCredentialOfferResult).toBeSuccessful(); + + const server = URL.parse("https://kc-openid4vc.is.enmeshed.eu/realms/enmeshed-openid4vci")!; + const clientId = "wallet"; + const config: client.Configuration = await client.discovery(server, clientId); + const grantReq = await client.genericGrantRequest(config, "password", { + username: "test", + password: "test", + scope: "wallet-demo" + }); - expect(result).toBeSuccessful(); + const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer: resolveCredentialOfferResult.value.credentialOffer, + credentialConfigurationIds: [eudiploCredentialConfigurationId], + accessToken: grantReq.access_token + }); + expect(credentialResponsesResult).toBeSuccessful(); +}); - const credentialOffer = result.value.credentialOffer; - const requestedCredentials = credentialOffer.credentialOfferPayload.credential_configuration_ids; +// TODO: unskip once fix to CanCreateShareCredentialOffer has been deployed to the connector +// eslint-disable-next-line jest/no-disabled-tests +test.skip("issuance with request", async () => { + const oldCredentials = ( + await runtimeServices1.consumption.attributes.getAttributes({ + query: { + "content.value.@type": "VerifiableCredential" + } + }) + ).value; - const wrongPinRequestResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer, - credentialConfigurationIds: requestedCredentials, - pinCode: `1${pin}` - }); - expect(wrongPinRequestResult.isError).toBe(true); + const sentMessage = ( + await serviceAxiosInstance.post("/enmeshed-demo/credential", { + recipient: runtimeServices1.address, + credentialConfigurationId: eudiploCredentialConfigurationId + }) + ).data.result; - const requestResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer, - credentialConfigurationIds: requestedCredentials, - pinCode: pin - }); - expect(requestResult).toBeSuccessful(); + const requestId = (sentMessage.content as RequestJSON).id!; + await syncUntilHasMessageWithRequest(runtimeServices1.transport, requestId); + await runtimeServices1.consumption.incomingRequests.accept({ + requestId, + items: [{ accept: true }] }); - // external authentication buggy in the latest release (1.16.0) - // eslint-disable-next-line jest/no-disabled-tests - test.skip("issuance with external authentication", async () => { - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "authorization_code" - }) - ).uri; + const currentCredentials = ( + await runtimeServices1.consumption.attributes.getAttributes({ + query: { + "content.value.@type": "VerifiableCredential" + } + }) + ).value; + expect(currentCredentials).toHaveLength(oldCredentials.length + 1); - const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); - expect(resolveCredentialOfferResult).toBeSuccessful(); + const oldCredentialIds = oldCredentials.map((c) => c.id); + const createdCredential = currentCredentials.find((c) => !oldCredentialIds.includes(c.id)); + expect(createdCredential).toBeDefined(); - const server = URL.parse("https://kc-openid4vc.is.enmeshed.eu/realms/enmeshed-openid4vci")!; - const clientId = "wallet"; - const config: client.Configuration = await client.discovery(server, clientId); - const grantReq = await client.genericGrantRequest(config, "password", { - username: "test", - password: "test", - scope: "wallet-demo" - }); + const credentialContent = createdCredential!.content.value as VerifiableCredentialJSON; + const decodedCredential = decodeRecord(credentialContent.type, credentialContent.value) as SdJwtVcRecord; + expect(decodedCredential.firstCredential.prettyClaims.givenName).toBe("aGivenName"); + expect(credentialContent.value.split("~")).toHaveLength(3); // given name is selectively disclosable, hence length 3 +}); - const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer: resolveCredentialOfferResult.value.credentialOffer, - credentialConfigurationIds: [eudiploCredentialConfigurationId], - accessToken: grantReq.access_token - }); - expect(credentialResponsesResult).toBeSuccessful(); +describe("presentation", () => { + let storedCredential: LocalAttributeDTO; + + beforeAll(async () => { + storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); }); - test("presentation", async () => { + test("standard presentation", async () => { const authorizationRequestUrl = ( await eudiploClient.createPresentationRequest({ responseType: "uri", @@ -169,43 +217,6 @@ describe("EUDIPLO", () => { const loadResult = await runtimeServices1.consumption.openId4Vc.resolveAuthorizationRequest({ authorizationRequestUrl }); const matchingCredentials = loadResult.value.matchingCredentials; - expect(matchingCredentials).toHaveLength(1); - - const queryResult = loadResult.value.authorizationRequest.dcql!.queryResult; - expect(queryResult.can_be_satisfied).toBe(true); - - const presentationResult = await runtimeServices1.consumption.openId4Vc.acceptAuthorizationRequest({ - authorizationRequest: loadResult.value.authorizationRequest, - attributeId: matchingCredentials[0].id - }); - expect(presentationResult).toBeSuccessful(); - expect(presentationResult.value.status).toBe(200); - }); - - // TODO: unskip once fix to CanCreateShareCredentialOffer has been deployed to the connector - // eslint-disable-next-line jest/no-disabled-tests - test.skip("issuance with request", async () => { - const oldCredentials = ( - await runtimeServices1.consumption.attributes.getAttributes({ - query: { - "content.value.@type": "VerifiableCredential" - } - }) - ).value; - - const sentMessage = ( - await serviceAxiosInstance.post("/enmeshed-demo/credential", { - recipient: runtimeServices1.address, - credentialConfigurationId: eudiploCredentialConfigurationId - }) - ).data.result; - - const requestId = (sentMessage.content as RequestJSON).id!; - await syncUntilHasMessageWithRequest(runtimeServices1.transport, requestId); - await runtimeServices1.consumption.incomingRequests.accept({ - requestId, - items: [{ accept: true }] - }); const currentCredentials = ( await runtimeServices1.consumption.attributes.getAttributes({ @@ -214,16 +225,17 @@ describe("EUDIPLO", () => { } }) ).value; - expect(currentCredentials).toHaveLength(oldCredentials.length + 1); + expect(matchingCredentials).toHaveLength(currentCredentials.length); - const oldCredentialIds = oldCredentials.map((c) => c.id); - const createdCredential = currentCredentials.find((c) => !oldCredentialIds.includes(c.id)); - expect(createdCredential).toBeDefined(); + const queryResult = loadResult.value.authorizationRequest.dcql!.queryResult; + expect(queryResult.can_be_satisfied).toBe(true); - const credentialContent = createdCredential!.content.value as VerifiableCredentialJSON; - const decodedCredential = decodeRecord(credentialContent.type, credentialContent.value) as SdJwtVcRecord; - expect(decodedCredential.firstCredential.prettyClaims.givenName).toBe("aGivenName"); - expect(credentialContent.value.split("~")).toHaveLength(3); // given name is selectively disclosable, hence length 3 + const presentationResult = await runtimeServices1.consumption.openId4Vc.acceptAuthorizationRequest({ + authorizationRequest: loadResult.value.authorizationRequest, + attributeId: matchingCredentials[0].id + }); + expect(presentationResult).toBeSuccessful(); + expect(presentationResult.value.status).toBe(200); }); test("presentation with request", async () => { @@ -254,64 +266,135 @@ describe("EUDIPLO", () => { expect(sessionStatus).toBe("completed"); // in case of failed presentation: Status remains "active" }); - test("create presentation token", async () => { - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "pre_authorized_code" - }) - ).uri; + describe("presentation token", () => { + test("create presentation token", async () => { + const createPresentationTokenResult = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ + attributeId: storedCredential.id, + expiresAt: CoreDate.utc().add({ minutes: 1 }).toString(), + ephemeral: true + }); + + expect(createPresentationTokenResult).toBeSuccessful(); + + const presentationTokenContent = createPresentationTokenResult.value.content; + expect(presentationTokenContent).toBeDefined(); + expect(presentationTokenContent["@type"]).toBe("TokenContentVerifiablePresentation"); + expect((presentationTokenContent as TokenContentVerifiablePresentationJSON).value).toBeDefined(); + expect((presentationTokenContent as TokenContentVerifiablePresentationJSON).displayInformation).toBeDefined(); + expect((presentationTokenContent as TokenContentVerifiablePresentationJSON).displayInformation![0].name).toBe("test"); + }); - const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); - const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer: resolveCredentialOfferResult.value.credentialOffer, - credentialConfigurationIds: [eudiploCredentialConfigurationId] + test("verify presentation token", async () => { + const presentationToken = await createPresentationToken(storedCredential); + + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + token: presentationToken + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(true); }); - const storedCredential = ( - await runtimeServices1.consumption.openId4Vc.storeCredentials({ - credentialResponses: credentialResponsesResult.value.credentialResponses - }) - ).value; - expect((storedCredential.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); - const presentationTokenResult = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ - attributeId: storedCredential.id, - expiresAt: CoreDate.utc().add({ minutes: 1 }).toString(), - ephemeral: true + test("fail token verification in case of invalid nonce", async () => { + const presentationToken = await createPresentationToken(storedCredential); + + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + token: { ...presentationToken, id: "TOKXXXXXXXXXXXXXXXXX" } + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(false); + expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid Nonce"); + }); + + test("fail token verification in case of invalid signature", async () => { + const presentationToken = await createPresentationToken(storedCredential); + + const tokenContentWithTamperedSignature = tamperSignatureOfTokenContent(presentationToken.content as TokenContentVerifiablePresentationJSON); + + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + token: { ...presentationToken, content: tokenContentWithTamperedSignature } + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(false); + expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid JWT Signature"); }); - expect(presentationTokenResult).toBeSuccessful(); - - const presentationTokenContent = presentationTokenResult.value.content; - expect(presentationTokenContent).toBeDefined(); - expect(presentationTokenContent["@type"]).toBe("TokenContentVerifiablePresentation"); - expect((presentationTokenContent as TokenContentVerifiablePresentation).value).toBeDefined(); - expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation).toBeDefined(); - expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation![0].name).toBe("test"); }); }); +async function createPresentationToken(storedCredential: LocalAttributeDTO) { + const result = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ + attributeId: storedCredential.id, + expiresAt: CoreDate.utc().add({ minutes: 1 }).toString(), + ephemeral: true + }); + + return result.value; +} + +async function createAndStoreCredential(eudiploClient: EudiploClient, eudiploCredentialConfigurationId: string) { + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code" + }) + ).uri; + + const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); + const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer: resolveCredentialOfferResult.value.credentialOffer, + credentialConfigurationIds: [eudiploCredentialConfigurationId] + }); + const storedCredential = ( + await runtimeServices1.consumption.openId4Vc.storeCredentials({ + credentialResponses: credentialResponsesResult.value.credentialResponses + }) + ).value; + return storedCredential; +} + +function tamperSignatureOfTokenContent(tokenContent: TokenContentVerifiablePresentationJSON): TokenContentVerifiablePresentationJSON { + const splittedValue = tokenContent.value.split("."); + + const header = splittedValue[0]; + const payload = splittedValue[1]; + const disclosure = splittedValue[3]; + const keyBindingJWT = splittedValue[4]; + + // the following is a signature of some old SD-JWT that we use here just to have a signature that is valid in structure but does not match the + // header and payload of the token content, thus leading to a failed verification due to invalid signature + const tamperedSignature = "V6RFMHpLyj2NOi4BphSygcbXxWvBeArY9zdkUGj-ERJO9S3CgGxst8lGyV0DJMT7N_-85kIDcukHDw2ia9KITQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9"; + + const tamperedTokenContent = { + ...tokenContent, + value: `${header}.${payload}.${tamperedSignature}.${disclosure}.${keyBindingJWT}` + }; + return tamperedTokenContent; +} + async function startOid4VcComposeStack() { - let baseUrl = process.env.NMSHD_TEST_BASEURL!; - let addressGenerationHostnameOverride: string | undefined; + const baseUrl = process.env.NMSHD_TEST_BASEURL!; + + const composeEnvironment = { + // eslint-disable-next-line @typescript-eslint/naming-convention + TEST_ENVIRONMENT: "container", + // eslint-disable-next-line @typescript-eslint/naming-convention + NMSHD_TEST_BASEURL: baseUrl + } as Record; if (baseUrl.includes("localhost")) { - addressGenerationHostnameOverride = "localhost"; - baseUrl = baseUrl.replace("localhost", "host.docker.internal"); + composeEnvironment.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE = "localhost"; + composeEnvironment.NMSHD_TEST_BASEURL = baseUrl.replace("localhost", "host.docker.internal"); + } else if (process.env.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE) { + composeEnvironment.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE = process.env.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE; } const composeFolder = path.resolve(path.join(__dirname, "..", "..", "..", "..", ".dev")); const composeStack = await new DockerComposeEnvironment(composeFolder, "compose.openid4vc.yml") .withProjectName("runtime-oid4vc-tests") - .withEnvironment({ - // eslint-disable-next-line @typescript-eslint/naming-convention - TEST_ENVIRONMENT: "container", - // eslint-disable-next-line @typescript-eslint/naming-convention - NMSHD_TEST_BASEURL: baseUrl, - - // eslint-disable-next-line @typescript-eslint/naming-convention - NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE: addressGenerationHostnameOverride - } as Record) + .withEnvironment(composeEnvironment) .withStartupTimeout(60000) .withWaitStrategy("oid4vc-service-1", Wait.forHealthCheck()) .up(); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index d4e68ba12..b5f83a5a3 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -3,5 +3,5 @@ "compilerOptions": { "noEmit": true }, - "include": ["packages/*/src", "packages/*/test"] + "include": ["packages/*/src", "packages/*/test", ".dev/eudiplo"] }