diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..4d6acff --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,97 @@ +name: Continuous Deployment + +on: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + APP: app + AGENT: agent + +jobs: + build-and-push-image-agent: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + permissions: + contents: read + packages: write + + defaults: + run: + working-directory: ${{ env.AGENT }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - run: AGENT_HOST=https://funke.animo.id pnpm build + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.AGENT }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: ./agent + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + build-and-push-image-app: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + permissions: + contents: read + packages: write + + defaults: + run: + working-directory: ${{ env.APP }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - run: NEXT_PUBLIC_API_URL=https://funke.animo.id pnpm build + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.APP }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: ./app + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/continuous_deployment.yaml b/.github/workflows/continuous_deployment.yaml deleted file mode 100644 index adb535c..0000000 --- a/.github/workflows/continuous_deployment.yaml +++ /dev/null @@ -1,135 +0,0 @@ -name: Continuous Deployment - -on: - workflow_dispatch: - inputs: - build: - default: true - type: boolean - required: false - description: Build the website before deploying -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - APP: app - AGENT: agent - -jobs: - build-and-push-image-agent: - runs-on: ubuntu-latest - if: inputs.build == true && github.ref == 'refs/heads/main' - permissions: - contents: read - packages: write - - defaults: - run: - working-directory: ${{ env.AGENT }} - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.AGENT }} - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: ./agent - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - build-and-push-image-app: - runs-on: ubuntu-latest - if: inputs.build == true && github.ref == 'refs/heads/main' - permissions: - contents: read - packages: write - - defaults: - run: - working-directory: ${{ env.APP }} - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - uses: pnpm/action-setup@v2 - with: - version: 8 - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - run: NEXT_PUBLIC_API_URL=https://openid4vc.animo.id pnpm build - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.APP }} - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: ./app - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - deploy: - # Only run on main branch - runs-on: ubuntu-latest - needs: [build-and-push-image-agent, build-and-push-image-app] - if: | - always() && - (needs.build-and-push-image-agent.result == 'success' || needs.build-and-push-image-agent.result == 'skipped') && - (needs.build-and-push-image-app.result == 'success' || needs.build-and-push-image-app.result == 'skipped') && - github.ref == 'refs/heads/main' - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Copy stack file to remote - uses: garygrossgarten/github-action-scp@v0.7.3 - with: - local: docker-compose.yml - remote: openid4vc-playground/docker-compose.yml - host: dashboard.dev.animo.id - username: root - privateKey: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }} - - - name: Deploy to Docker Swarm via SSH action - uses: appleboy/ssh-action@v0.1.4 - env: - AGENT_WALLET_KEY: ${{ secrets.AGENT_WALLET_KEY }} - CHEQD_TESTNET_COSMOS_PAYER_SEED: ${{ secrets.CHEQD_TESTNET_COSMOS_PAYER_SEED }} - DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED: ${{ secrets.DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED }} - P256_SEED: ${{ secrets.P256_SEED }} - ED25519_SEED: ${{ secrets.ED25519_SEED }} - with: - host: dashboard.dev.animo.id - username: root - key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }} - envs: P256_SEED,ED25519_SEED,AGENT_WALLET_KEY,DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED,CHEQD_TESTNET_COSMOS_PAYER_SEED - script: | - ED25519_SEED=${ED25519_SEED} P256_SEED=${P256_SEED} AGENT_WALLET_KEY=${AGENT_WALLET_KEY} CHEQD_TESTNET_COSMOS_PAYER_SEED=${CHEQD_TESTNET_COSMOS_PAYER_SEED} DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED=${DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED} docker stack deploy --compose-file openid4vc-playground/docker-compose.yml openid4vc-playground --with-registry-auth diff --git a/.gitignore b/.gitignore index 1bd5675..df78cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ +ngrok.auth.yml node_modules .DS_Store dist .env -.env.local \ No newline at end of file +.env.local +.next +out diff --git a/README.md b/README.md index f924605..8b61cbf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

-

Animo OpenID4VC Playground

+

Funke OpenID4VC Playground

> [!TIP] -> Check out the demo at https://openid4vc.animo.id +> Check out the demo at https://funke.animo.id ## ✨ Hi there! -Welcome to the repository of Animo's OpenID4VC Playground. This interactive playground demonstrates the use of OpenID4VC with different credential formats (such as SD-JWT VCs). This demo is built using [Aries Framework Javascript (AFJ)](https://github.com/hyperledger/aries-framework-javascript). AFJ is a framework written in TypeScript for building decentralized identity services. +Welcome to the repository of Animo's OpenID4VC Playground. This interactive playground demonstrates the use of OpenID4VC with different credential formats (such as SD-JWT VCs). This demo is built using [Credo](https://github.com/openwallet-foundation/credo-ts). Credo is a framework written in TypeScript for building decentralized identity services. ## 🛠️ Usage @@ -57,8 +57,7 @@ cp .env.example .env | `AGENT_WALLET_KEY` | Used in the backend application for the agent. Should be secure and kept private. | > [!IMPORTANT] -> The issuer will use `did:web` for issuing credentials, but this requires `https` to be used. When developing locally it is recommend -> to use `ngrok` (`npx ngrok http 3001`) and use that url as the `AGENT_HOST` variable. Make sure to also set the `NEXT_PUBLIC_API_URL` variable in the app to the ngrok. +> You can use `ngrok` (`npx ngrok http 3001`) and use that url as the `AGENT_HOST` variable. Make sure to also set the `NEXT_PUBLIC_API_URL` variable in the app to the ngrok. > > We may add issuance using did:key in development if the host url does not start with `https`. @@ -80,7 +79,7 @@ Copy the https url from the ngrok command and set that as the `AGENT_HOST` ```bash cd agent -AGENT_HOST=https://30f9-58-136-114-148.ngrok-free.app pnpm dev +AGENT_HOST=https://ebcf-161-51-75-237.ngrok-free.app pnpm dev ``` ```bash diff --git a/agent/.env.development b/agent/.env.development index 5b8c777..bc5d02c 100644 --- a/agent/.env.development +++ b/agent/.env.development @@ -1,5 +1,5 @@ AGENT_WALLET_KEY=secret-wallet-key -DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED=2543786a945a27258087ccfe95ff62df -CHEQD_TESTNET_COSMOS_PAYER_SEED=robust across amount corn curve panther opera wish toe ring bleak empower wreck party abstract glad average muffin picnic jar squeeze annual long aunt -ED25519_SEED=5473a3e4c5ae3fd5fb3ad089563596e3 -P256_SEED=e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962239bbcf42f \ No newline at end of file +ROOT_P256_SEED=e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962hgytbcf42f +DCS_P256_SEED=e5f18b10aaa5cdb76818bc6ae8b71eb475e6eac76875ed085d3962hgytbcf42e +ANTHROPIC_API_KEY= +AGENT_HOST=http://localhost:3001/ diff --git a/agent/.env.example b/agent/.env.example index 2cc38ef..ced2168 100644 --- a/agent/.env.example +++ b/agent/.env.example @@ -2,5 +2,8 @@ AGENT_HOST=http://localhost:3001 AGENT_WALLET_KEY=secret-wallet-key DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED=2543786a945a27258087ccfe95ff62df CHEQD_TESTNET_COSMOS_PAYER_SEED=robust across amount corn curve panther opera wish toe ring bleak empower wreck party abstract glad average muffin picnic jar squeeze annual long aunt -ED25519_SEED=5473a3e4c5ae3fd5fb3ad089563596e3 -P256_SEED=e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962239bbcf42f \ No newline at end of file +ROOT_P256_SEED=e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962hgytbcf42f +DCS_P256_SEED=e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962hgytbcf42e +X509_ROOT_CERTIFICATE=MIIB8TCCAZigAwIBAgIQFMfMaD9JG917I+r6ztmSmjAKBggqhkjOPQQDAjAdMQ4wDAYDVQQDEwVBbmltbzELMAkGA1UEBhMCTkwwHhcNMjQwMjE5MTEyNDIzWhcNMjgwMjE5MTEyNDIzWjAdMQ4wDAYDVQQDEwVBbmltbzELMAkGA1UEBhMCTkwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6mRp2xWn9lR8juDM4hIVHgX6HmVQTPlk8mZfg4jAuv3frEuDCo4G5MIG2MEoGA1UdDgRDBEEE3A9V8ynqRcVjADqlfpZ9X8mwbew0TuQldH/QOpkadsVp/ZUfI7gzOISFR4F+h5lUEz5ZPJmX4OIwLr936xLgwjAOBgNVHQ8BAf8EBAMCAQYwIQYDVR0SBBowGIYWaHR0cHM6Ly9mdW5rZS5hbmltby5pZDASBgNVHRMBAf8ECDAGAQH/AgEAMCEGA1UdHwQaMBgwFqAUoBKGEGh0dHBzOi8vYW5pbW8uaWQwCgYIKoZIzj0EAwIDRwAwRAIgFhmZ7WSPYMNT085jZSr2a/GtZ9x1OaR/iVE7v178NeACIGyH3ahZn/SKWrHuw+YT4AsFgexZeG1Z+2KrhhuVu8S7 +X509_DCS_CERTIFICATE=MIICQDCCAeagAwIBAgIQWGRYPwCkw3JO8J9blPTkLDAKBggqhkjOPQQDAjAdMQ4wDAYDVQQDEwVBbmltbzELMAkGA1UEBhMCTkwwHhcNMjQwMjE5MTEyNDIzWhcNMjgwMjE5MTEyNDIzWjAhMRIwEAYDVQQDEwljcmVkbyBkY3MxCzAJBgNVBAYTAk5MMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElpGfqraK5bXiisBkifcI67SvBIjkJOj08n1rdRw6nwGUwtyl3H1s5P38I1AgpyAYKUU0v9indKifYp4iecOxLaOCAQIwgf8wSgYDVR0OBEMEQQSWkZ+qtorlteKKwGSJ9wjrtK8EiOQk6PTyfWt1HDqfAZTC3KXcfWzk/fwjUCCnIBgpRTS/2Kd0qJ9iniJ5w7EtMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUBAf8ECzAJBgcogYxdBQECMEwGA1UdIwRFMEOAQQTcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6mRp2xWn9lR8juDM4hIVHgX6HmVQTPlk8mZfg4jAuv3frEuDCMCEGA1UdEgQaMBiGFmh0dHBzOi8vZnVua2UuYW5pbW8uaWQwGQYDVR0RBBIwEIIOZnVua2UuYW5pbW8uaWQwCgYIKoZIzj0EAwIDSAAwRQIhAOtIcYdgFgw/o8a5TKH2qIVN72AobFxS7uTHUcnFxG6xAiAc0vWgs8YJ73hGhNtTNVylvI/uUJ0w9gp/AGngdMcYfg== +ANTHROPIC_API_KEY= diff --git a/agent/Dockerfile b/agent/Dockerfile index 1ee08c9..6341b3a 100644 --- a/agent/Dockerfile +++ b/agent/Dockerfile @@ -1,20 +1,23 @@ -FROM node:18 as base +FROM node:20 AS base COPY package.json /app/package.json ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" +RUN npm install -g corepack@latest RUN corepack enable WORKDIR /app FROM base AS prod-deps COPY tsconfig.json /app/tsconfig.json +COPY patches /app/patches RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod FROM base AS build COPY tsconfig.json /app/tsconfig.json +COPY patches /app/patches RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install COPY src src @@ -24,6 +27,7 @@ FROM base COPY --from=prod-deps /app/node_modules /app/node_modules COPY --from=build /app/dist /app/dist +COPY assets /app/assets EXPOSE 3000 -CMD [ "pnpm", "start" ] \ No newline at end of file +CMD [ "node", "dist/server.js" ] diff --git a/agent/assets/erika.jpeg b/agent/assets/erika.jpeg new file mode 100644 index 0000000..1a3201f Binary files /dev/null and b/agent/assets/erika.jpeg differ diff --git a/agent/assets/signature.jpeg b/agent/assets/signature.jpeg new file mode 100644 index 0000000..90ef3f7 Binary files /dev/null and b/agent/assets/signature.jpeg differ diff --git a/agent/package.json b/agent/package.json index 3c83a47..6cf06df 100644 --- a/agent/package.json +++ b/agent/package.json @@ -1,27 +1,45 @@ { "name": "agent", + "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a", "dependencies": { - "@credo-ts/askar": "^0.5.1", - "@credo-ts/cheqd": "^0.5.1", - "@credo-ts/core": "^0.5.1", - "@credo-ts/indy-vdr": "^0.5.1", - "@credo-ts/node": "^0.5.1", - "@credo-ts/openid4vc": "^0.5.1", - "@hyperledger/aries-askar-nodejs": "^0.2.0", - "@hyperledger/indy-vdr-nodejs": "^0.2.0", + "@ai-sdk/anthropic": "^1.1.14", + "@credo-ts/askar": "0.6.0-alpha-20250324131619", + "@credo-ts/core": "0.6.0-alpha-20250324131619", + "@credo-ts/node": "0.6.0-alpha-20250324131619", + "@credo-ts/openid4vc": "0.6.0-alpha-20250324131619", + "@openwallet-foundation/askar-nodejs": "^0.3.2", + "@openwallet-foundation/askar-shared": "^0.3.2", + "@peculiar/x509": "^1.12.3", + "ai": "^4.1.51", "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.18.2", - "zod": "^3.22.4" + "oidc-provider": "^8.5.3", + "zod": "3.24.1" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/node": "^20.11.7", + "@types/oidc-provider": "^8.5.2", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "~5.3.3" }, "scripts": { "build": "tsc -p tsconfig.json", "start": "node dist/server.js", "dev": "tsx watch -r dotenv/config src/server.ts dotenv_config_path=.env.development" + }, + "pnpm": { + "overrides": { + "@credo-ts/core": "npm:@animo-id/credo-ts-core@0.5.14-alpha-20250508212929", + "@credo-ts/node": "npm:@animo-id/credo-ts-node@0.5.14-alpha-20250508212929", + "@credo-ts/askar": "npm:@animo-id/credo-ts-askar@0.5.14-alpha-20250508212929", + "@credo-ts/openid4vc": "npm:@animo-id/credo-ts-openid4vc@0.5.14-alpha-20250508212929" + }, + "patchedDependencies": { + "@animo-id/credo-ts-core": "patches/@animo-id__credo-ts-core.patch", + "@animo-id/credo-ts-openid4vc": "patches/@animo-id__credo-ts-openid4vc.patch" + } } } diff --git a/agent/patches/@animo-id__credo-ts-core.patch b/agent/patches/@animo-id__credo-ts-core.patch new file mode 100644 index 0000000..07be751 --- /dev/null +++ b/agent/patches/@animo-id__credo-ts-core.patch @@ -0,0 +1,44 @@ +diff --git a/build/modules/vc/W3cCredentialsModule.js b/build/modules/vc/W3cCredentialsModule.js +index 355945b9f5e4a1ec1cca43dcdeb122891c060c98..9642bc83b257a0b2db00b56640c165063cc93967 100644 +--- a/build/modules/vc/W3cCredentialsModule.js ++++ b/build/modules/vc/W3cCredentialsModule.js +@@ -27,16 +27,16 @@ class W3cCredentialsModule { + dependencyManager.registerSingleton(SignatureSuiteRegistry_1.SignatureSuiteRegistry); + // Register the config + dependencyManager.registerInstance(W3cCredentialsModuleConfig_1.W3cCredentialsModuleConfig, this.config); +- // Always register ed25519 signature suite +- dependencyManager.registerInstance(SignatureSuiteRegistry_1.SignatureSuiteToken, { +- suiteClass: signature_suites_1.Ed25519Signature2018, +- proofType: 'Ed25519Signature2018', +- verificationMethodTypes: [ +- dids_1.VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, +- dids_1.VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, +- ], +- keyTypes: [crypto_1.KeyType.Ed25519], +- }); ++ // // Always register ed25519 signature suite ++ // dependencyManager.registerInstance(SignatureSuiteRegistry_1.SignatureSuiteToken, { ++ // suiteClass: signature_suites_1.Ed25519Signature2018, ++ // proofType: 'Ed25519Signature2018', ++ // verificationMethodTypes: [ ++ // dids_1.VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, ++ // dids_1.VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, ++ // ], ++ // keyTypes: [crypto_1.KeyType.Ed25519], ++ // }); + dependencyManager.registerInstance(SignatureSuiteRegistry_1.SignatureSuiteToken, { + suiteClass: signature_suites_1.Ed25519Signature2020, + proofType: 'Ed25519Signature2020', +diff --git a/build/modules/x509/X509Service.js b/build/modules/x509/X509Service.js +index 7de83a3e1b5e7cc27e378a3a671792dbc28be236..dbcc1b902ffa4f31e4fc0242f6c4c811010e2121 100644 +--- a/build/modules/x509/X509Service.js ++++ b/build/modules/x509/X509Service.js +@@ -108,7 +108,7 @@ let X509Service = class X509Service { + // In this case we could skip the signature verification (not other verifications), as we already trust the signer certificate, + // but i think the purpose of ISO 18013-5 mDL is that you trust the root certificate. If we can't verify the whole chain e.g. + // when we receive a credential we have the chance it will fail later on. +- const skipSignatureVerification = i === 0 && trustedCertificates && cert.issuer !== cert.subject && !publicKey; ++ const skipSignatureVerification = i === 0 && trustedCertificates && !publicKey; + // NOTE: at some point we might want to change this to throw an error instead of skipping the signature verification of the trusted + // but it would basically prevent mDOCs from unknown issuers to be verified in the wallet. Verifiers should only trust the root certificate + // anyway. diff --git a/agent/patches/@animo-id__credo-ts-openid4vc.patch b/agent/patches/@animo-id__credo-ts-openid4vc.patch new file mode 100644 index 0000000..da8312f --- /dev/null +++ b/agent/patches/@animo-id__credo-ts-openid4vc.patch @@ -0,0 +1,12 @@ +diff --git a/build/openid4vc-verifier/OpenId4VpVerifierService.js b/build/openid4vc-verifier/OpenId4VpVerifierService.js +index b4d58d511d21f3f474fd27792f1f1e971149ad54..98e739ba927fe260968ba3fa0260279fc14a8e66 100644 +--- a/build/openid4vc-verifier/OpenId4VpVerifierService.js ++++ b/build/openid4vc-verifier/OpenId4VpVerifierService.js +@@ -157,6 +157,7 @@ let OpenId4VpVerifierService = class OpenId4VpVerifierService { + jwtSigner: jwtIssuer, + requestUri: hostedAuthorizationRequestUri, + expiresInSeconds: this.config.authorizationRequestExpiresInSeconds, ++ additionalJwtPayload: options.additionalJwtPayload + } + : undefined, + authorizationRequestPayload: requestParamsBase.response_mode === 'dc_api.jwt' || requestParamsBase.response_mode === 'dc_api' diff --git a/agent/src/agent.ts b/agent/src/agent.ts index 783cd68..a052a0e 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -1,122 +1,132 @@ -import { - Agent, - ConsoleLogger, - DidsModule, - JwkDidRegistrar, - JwkDidResolver, - KeyDidRegistrar, - KeyDidResolver, - LogLevel, - WebDidResolver, - joinUriParts, -} from "@credo-ts/core"; -import { agentDependencies } from "@credo-ts/node"; -import { AskarModule } from "@credo-ts/askar"; -import { - CheqdModule, - CheqdDidRegistrar, - CheqdDidResolver, -} from "@credo-ts/cheqd"; -import { - IndyVdrIndyDidRegistrar, - IndyVdrIndyDidResolver, - IndyVdrModule, -} from "@credo-ts/indy-vdr"; -import { indyVdr } from "@hyperledger/indy-vdr-nodejs"; -import { ariesAskar } from "@hyperledger/aries-askar-nodejs"; -import { - OpenId4VcHolderModule, - OpenId4VcIssuerModule, - OpenId4VcVerifierModule, -} from "@credo-ts/openid4vc"; -import { - AGENT_HOST, - AGENT_WALLET_KEY, - CHEQD_TESTNET_COSMOS_PAYER_SEED, -} from "./constants"; -import { Router } from "express"; -import { credentialRequestToCredentialMapper } from "./issuer"; +import { AskarModule } from '@credo-ts/askar' +import { Agent, ConsoleLogger, LogLevel, X509Module, joinUriParts } from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' +import { OpenId4VcHolderModule, OpenId4VcIssuerModule, OpenId4VcVerifierModule } from '@credo-ts/openid4vc' +import { askar } from '@openwallet-foundation/askar-nodejs' +import { Router } from 'express' +import { AGENT_HOST, AGENT_WALLET_KEY } from './constants' +import { credentialRequestToCredentialMapper, getVerificationSessionForIssuanceSession } from './issuer' +import { verifierTrustChains } from './verifiers' +import { getAuthorityHints, isSubordinateTo } from './verifiers/trustChains' -process.on("unhandledRejection", (reason) => { - console.log("Unhandled rejection", reason); -}); +import * as certs from './iaca-x509-certs' -export const openId4VciRouter = Router(); -export const openId4VpRouter = Router(); +process.on('unhandledRejection', (reason) => { + console.error('Unhandled rejection', reason) +}) + +export const openId4VciRouter = Router() +export const openId4VpRouter = Router() + +const x509PidIssuerRootCertificate = + 'MIICeTCCAiCgAwIBAgIUB5E9QVZtmUYcDtCjKB/H3VQv72gwCgYIKoZIzj0EAwIwgYgxCzAJBgNVBAYTAkRFMQ8wDQYDVQQHDAZCZXJsaW4xHTAbBgNVBAoMFEJ1bmRlc2RydWNrZXJlaSBHbWJIMREwDwYDVQQLDAhUIENTIElERTE2MDQGA1UEAwwtU1BSSU5EIEZ1bmtlIEVVREkgV2FsbGV0IFByb3RvdHlwZSBJc3N1aW5nIENBMB4XDTI0MDUzMTA2NDgwOVoXDTM0MDUyOTA2NDgwOVowgYgxCzAJBgNVBAYTAkRFMQ8wDQYDVQQHDAZCZXJsaW4xHTAbBgNVBAoMFEJ1bmRlc2RydWNrZXJlaSBHbWJIMREwDwYDVQQLDAhUIENTIElERTE2MDQGA1UEAwwtU1BSSU5EIEZ1bmtlIEVVREkgV2FsbGV0IFByb3RvdHlwZSBJc3N1aW5nIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYGzdwFDnc7+Kn5ibAvCOM8ke77VQxqfMcwZL8IaIA+WCROcCfmY/giH92qMru5p/kyOivE0RC/IbdMONvDoUyaNmMGQwHQYDVR0OBBYEFNRWGMCJOOgOWIQYyXZiv6u7xZC+MB8GA1UdIwQYMBaAFNRWGMCJOOgOWIQYyXZiv6u7xZC+MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA0cAMEQCIGEm7wkZKHt/atb4MdFnXW6yrnwMUT2u136gdtl10Y6hAiBuTFqvVYth1rbxzCP0xWZHmQK9kVyxn8GPfX27EIzzsw==' +const x509PidIssuerCertificate = + 'MIICdDCCAhugAwIBAgIBAjAKBggqhkjOPQQDAjCBiDELMAkGA1UEBhMCREUxDzANBgNVBAcMBkJlcmxpbjEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxETAPBgNVBAsMCFQgQ1MgSURFMTYwNAYDVQQDDC1TUFJJTkQgRnVua2UgRVVESSBXYWxsZXQgUHJvdG90eXBlIElzc3VpbmcgQ0EwHhcNMjQwNTMxMDgxMzE3WhcNMjUwNzA1MDgxMzE3WjBsMQswCQYDVQQGEwJERTEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxCjAIBgNVBAsMAUkxMjAwBgNVBAMMKVNQUklORCBGdW5rZSBFVURJIFdhbGxldCBQcm90b3R5cGUgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOFBq4YMKg4w5fTifsytwBuJf/7E7VhRPXiNm52S3q1ETIgBdXyDK3kVxGxgeHPivLP3uuMvS6iDEc7qMxmvduKOBkDCBjTAdBgNVHQ4EFgQUiPhCkLErDXPLW2/J0WVeghyw+mIwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwLQYDVR0RBCYwJIIiZGVtby5waWQtaXNzdWVyLmJ1bmRlc2RydWNrZXJlaS5kZTAfBgNVHSMEGDAWgBTUVhjAiTjoDliEGMl2Yr+ru8WQvjAKBggqhkjOPQQDAgNHADBEAiAbf5TzkcQzhfWoIoyi1VN7d8I9BsFKm1MWluRph2byGQIgKYkdrNf2xXPjVSbjW/U/5S5vAEC5XxcOanusOBroBbU=' +const x509BdrMdlIssuerCertificate = `-----BEGIN CERTIFICATE----- +MIICKTCCAc+gAwIBAgIUbsApBxL0COE3jxup8qQBlIlEy/8wCgYIKoZIzj0EAwIw +PTELMAkGA1UEBhMCREUxLjAsBgNVBAMMJUJEUiBJQUNBIElTTy9JRUMgMTgwMTMt +NSB2MSBURVNULU9OTFkwHhcNMjUwMTIxMTQyOTM0WhcNMjYwMTIxMTQyOTM0WjA7 +MQswCQYDVQQGEwJERTEsMCoGA1UEAwwjQkRSIERTIElTTy9JRUMgMTgwMTMtNSB2 +MSBURVNULU9OTFkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR9vm6Dm0/6x/VO +p/cpv3+r9Cqen4WN6Ap8N2f899d1aGmTA0hwAw143Dj5AiJtVRNwurjld35+Fu7c +tHzS7RKto4GuMIGrMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUBAf8ECzAJBgcogYxd +BQECMB0GA1UdEgQWMBSBEm1kbC1leGFtcGxlQGJkci5kZTAjBgNVHR8EHDAaMBig +FqAUghJtZGwuZXhhbXBsZS5iZHIuZGUwHwYDVR0jBBgwFoAUl4pXNIj/CSM7oB/e +eYOb4ScpQGkwHQYDVR0OBBYEFBcVf+pAFlU5mTWz63oyb9yx7UsEMAoGCCqGSM49 +BAMCA0gAMEUCIQD4pFHRCnOkuP4l1GHy66dd60bLkRNsQbHOFvYE7OP44QIgJHyJ +/TOG+Co5yNXYLwzaTtgQ4mNnm/uqjepnMHm02Bc= +-----END CERTIFICATE-----` export const agent = new Agent({ dependencies: agentDependencies, config: { - label: "OpenID4VC Playground", + label: 'OpenID4VC Playground', logger: new ConsoleLogger(LogLevel.trace), // TODO: add postgres storage walletConfig: { - id: "openid4vc-playground", + id: 'openid4vc-playground', key: AGENT_WALLET_KEY, }, }, modules: { - cheqd: new CheqdModule({ - networks: [ - { - network: "testnet", - cosmosPayerSeed: CHEQD_TESTNET_COSMOS_PAYER_SEED, - }, - ], - }), - indyVdr: new IndyVdrModule({ - indyVdr, - networks: [ - { - genesisTransactions: `{"reqSignature":{},"txn":{"data":{"data":{"alias":"OpsNode","blskey":"4i39oJqm7fVX33gnYEbFdGurMtwYQJgDEYfXdYykpbJMWogByocaXxKbuXdrg3k9LP33Tamq64gUwnm4oA7FkxqJ5h4WfKH6qyVLvmBu5HgeV8Rm1GJ33mKX6LWPbm1XE9TfzpQXJegKyxHQN9ABquyBVAsfC6NSM4J5t1QGraJBfZi","blskey_pop":"Qq3CzhSfugsCJotxSCRAnPjmNDJidDz7Ra8e4xvLTEzQ5w3ppGray9KynbGPH8T7XnUTU1ioZadTbjXaRY26xd4hQ3DxAyR4GqBymBn3UBomLRJHmj7ukcdJf9WE6tu1Fp1EhxmyaMqHv13KkDrDfCthgd2JjAWvSgMGWwAAzXEow5","client_ip":"13.58.197.208","client_port":"9702","node_ip":"3.135.134.42","node_port":"9701","services":["VALIDATOR"]},"dest":"EVwxHoKXUy2rnRzVdVKnJGWFviamxMwLvUso7KMjjQNH"},"metadata":{"from":"Pms5AZzgPWHSj6nNmJDfmo"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"77ad6682f320be9969f70a37d712344afed8e3fba8d43fa5602c81b578d26088"},"ver":"1"} -{"reqSignature":{},"txn":{"data":{"data":{"alias":"cynjanode","blskey":"32DLSweyJRxVMcVKGjUeNkVF1fwyFfRcFqGU9x7qL2ox2STpF6VxZkbxoLkGMPnt3gywRaY6jAjqgC8XMkf3webMJ4SEViPtBKZJjCCFTf4tGXfEsMwinummaPja85GgTALf7DddCNyCojmkXWHpgjrLx3626Z2MiNxVbaMapG2taFX","blskey_pop":"RQRU8GVYSYZeu9dfH6myhzZ2qfxeVpCL3bTzgto1bRbx3QCt3mFFQQBVbgrqui2JpXhcWXxoDzp1WyYbSZwYqYQbRmvK7PPG82VAvVagv1n83Qa3cdyGwCevZdEzxuETiiXBRWSPfb4JibAXPKkLZHyQHWCEHcAEVeXtx7FRS1wjTd","client_ip":"3.17.103.221","client_port":"9702","node_ip":"3.17.215.226","node_port":"9701","services":["VALIDATOR"]},"dest":"iTq944JTtwHnst7rucfsRA4m26x9i6zCKKohETBCiWu"},"metadata":{"from":"QC174PGaL4zA9YHYqofPH2"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"ce7361e44ec10a275899ece1574f6e38f2f3c7530c179fa07a2924e55775759b"},"ver":"1"} -{"reqSignature":{},"txn":{"data":{"data":{"alias":"GlobaliD","blskey":"4Behdr1KJfLTAPNospghtL7iWdCHca6MZDxAtzYNXq35QCUr4aqpLu6p4Sgu9wNbTACB3DbwmVgE2L7hX6UsasuvZautqUpf4nC5viFpH7X6mHyqLreBJTBH52tSwifQhRjuFAySbbfyRK3wb6R2Emxun9GY7MFNuy792LXYg4C6sRJ","blskey_pop":"RKYDRy8oTxKnyAV3HocapavH2jkw3PVe54JcEekxXz813DFbEy87N3i3BNqwHB7MH93qhtTRb7EZMaEiYhm92uaLKyubUMo5Rqjve2jbEdYEYVRmgNJWpxFKCmUBa5JwBWYuGunLMZZUTU3qjbdDXkJ9UNMQxDULCPU5gzLTy1B5kb","client_ip":"13.56.175.126","client_port":"9702","node_ip":"50.18.84.131","node_port":"9701","services":["VALIDATOR"]},"dest":"2ErWxamsNGBfhkFnwYgs4UW4aApct1kHUvu7jbkA1xX4"},"metadata":{"from":"4H8us7B1paLW9teANv8nam"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"0c3b33b77e0419d6883be35d14b389c3936712c38a469ac5320a3cae68be1293"},"ver":"1"} -{"reqSignature":{},"txn":{"data":{"data":{"alias":"IdRamp","blskey":"LoYzqUMPDZEfRshwGSzkgATxcM5FAS1LYx896zHnMfXP7duDsCQ6CBG2akBkZzgH3tBMvnjhs2z7PFc2gFeaKUF9fKDHhtbVqPofxH3ebcRfA959qU9mgvmkUwMUgwd21puRU6BebUwBiYxMxcE5ChReBnAkdAv19gVorm3prBMk94","blskey_pop":"R1DjpsG7UxgwstuF7WDUL17a9Qq64vCozwJZ88bTrSDPwC1cdRn3WmhqJw5LpEhFQJosDSVVT6tS8dAZrrssRv2YsELbfGEJ7ZGjhNjZHwhqg4qeustZ7PZZE3Vr1ALSHY4Aa6KpNzGodxu1XymYZWXAFokPAs3Kho8mKcJwLCHn3h","client_ip":"207.126.128.12","client_port":"9702","node_ip":"207.126.129.12","node_port":"9701","services":["VALIDATOR"]},"dest":"5Zj5Aec6Kt9ki1runrXu87wZ522mnm3zwmaoHLUcHLx9"},"metadata":{"from":"AFLDFPoJuDQUHqnfmg8U7i"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"c9df105558333ac8016610d9da5aad1e9a5dd50b9d9cc5684e94f439fa10f836"},"ver":"1"} -{"reqSignature":{},"txn":{"data":{"data":{"alias":"idlab-node01","blskey":"2fjJVi33U1tCTjW77cJaf1NLz7EzWkVNzR9BEQpVVK64MJpRKNUzt6k7Td2U8yqU5hGyAFH5N7ZymSB55TnpC3rJYLVTcGXZeXpmrQx3mwnXNyfTDnxfTpdQ1KMoFeZoDPZ8acfaH8GWeW2jL1qREE52tetBf4tXTeshmWzGkEN7r4y","blskey_pop":"RSjiM6dYUmN2rv2ca7dUCmEKrivq12rhxhXUKHdmSwUxbCmcijsgoERjYG7MqxhKLjSAJ5715K23fVEc6uK1kTenKmYCcCts8MLMAQG8Upb22nfgHJ3py8RwRoACeAjFF3myAMNRJJPhUdv96drJdwkGRv7f6JjvoB5KWVQYTNgheP","client_ip":"205.159.92.17","client_port":"9702","node_ip":"205.159.92.16","node_port":"9701","services":["VALIDATOR"]},"dest":"8czYgwmLDazVrBHuo53Tyx7Tw8ZhvnoC2BfhQGir4r8F"},"metadata":{"from":"PN8wFxLKjdkwyxoEEXwyz2"},"type":"0"},"txnMetadata":{"seqNo":5,"txnId":"9237eca7d2a203f6e1779f63064d2f22cf28e1bcd4e6fe5d791b15e82969acdc"},"ver":"1"} -{"reqSignature":{},"txn":{"data":{"data":{"alias":"lorica-identity-node1","blskey":"wUh24sVCQ8PHDgSb343g2eLxjD5vwxsrETfuV2sbwMNnYon9nhbaK5jcWTekvXtyiwxHxuiCCoZwKS97MQEAeC2oLbbMeKjYm212QwSnm7aKLEqTStXht35VqZvZLT7Q3mPQRYLjMGixdn4ocNHrBTMwPUQYycEqwaHWgE1ncDueXY","blskey_pop":"R2sMwF7UW6AaD4ALa1uB1YVPuP6JsdJ7LsUoViM9oySFqFt34C1x1tdHDysS9wwruzaaEFui6xNPqJ8eu3UBqcFKkoWhdsMqCALwe63ytxPwvtLtCffJLhHAcgrPC7DorXYdqhdG2cevdqc5oqFEAaKoFDBf12p5SsbbM4PYWCmVCb","client_ip":"35.225.220.151","client_port":"9702","node_ip":"35.224.26.110","node_port":"9701","services":["VALIDATOR"]},"dest":"k74ZsZuUaJEcB8RRxMwkCwdE5g1r9yzA3nx41qvYqYf"},"metadata":{"from":"Ex6hzsJFYzNJ7kzbfncNeU"},"type":"0"},"txnMetadata":{"seqNo":6,"txnId":"6880673ce4ae4a2352f103d2a6ae20469dd070f2027283a1da5e62a64a59d688"},"ver":"1"} -{"reqSignature":{},"txn":{"data":{"data":{"alias":"cysecure-itn","blskey":"GdCvMLkkBYevRFi93b6qaj9G2u1W6Vnbg8QhRD1chhrWR8vRE8x9x7KXVeUBPFf6yW5qq2JCfA2frc8SGni2RwjtTagezfwAwnorLhVJqS5ZxTi4pgcw6smebnt4zWVhTkh6ugDHEypHwNQBcw5WhBZcEJKgNbyVLnHok9ob6cfr3u","blskey_pop":"RbH9mY7M5p3UB3oj4sT1skYwMkxjoUnja8eTYfcm83VcNbxC9zR9pCiRhk4q1dJT3wkDBPGNKnk2p83vaJYLcgMuJtzoWoJAWAxjb3Mcq8Agf6cgQpBuzBq2uCzFPuQCAhDS4Kv9iwA6FsRnfvoeFTs1hhgSJVxQzDWMVTVAD9uCqu","client_ip":"35.169.19.171","client_port":"9702","node_ip":"54.225.56.21","node_port":"9701","services":["VALIDATOR"]},"dest":"4ETBDmHzx8iDQB6Xygmo9nNXtMgq9f6hxGArNhQ6Hh3u"},"metadata":{"from":"uSXXXEdBicPHMMhr3ddNF"},"type":"0"},"txnMetadata":{"seqNo":7,"txnId":"3c21718b07806b2f193b35953dda5b68b288efd551dce4467ce890703d5ba549"},"ver":"1"}`, - indyNamespace: "indicio:testnet", - isProduction: false, - // FIXME: indy not fully working yet - connectOnStartup: false, - transactionAuthorAgreement: { - acceptanceMechanism: "for_session", - version: "1.0", - }, - }, - ], - }), - dids: new DidsModule({ - resolvers: [ - new KeyDidResolver(), - new JwkDidResolver(), - new IndyVdrIndyDidResolver(), - new WebDidResolver(), - new CheqdDidResolver(), - ], - registrars: [ - new KeyDidRegistrar(), - new JwkDidRegistrar(), - new IndyVdrIndyDidRegistrar(), - new CheqdDidRegistrar(), - ], - }), askar: new AskarModule({ - ariesAskar, + askar, }), openId4VcIssuer: new OpenId4VcIssuerModule({ - baseUrl: joinUriParts(AGENT_HOST, ["oid4vci"]), + baseUrl: joinUriParts(AGENT_HOST, ['oid4vci']), router: openId4VciRouter, - endpoints: { - credential: { - credentialRequestToCredentialMapper, - }, - }, + credentialRequestToCredentialMapper, + getVerificationSessionForIssuanceSessionAuthorization: getVerificationSessionForIssuanceSession, }), openId4VcHolder: new OpenId4VcHolderModule(), openId4VcVerifier: new OpenId4VcVerifierModule({ - baseUrl: joinUriParts(AGENT_HOST, ["siop"]), + baseUrl: joinUriParts(AGENT_HOST, ['oid4vp']), router: openId4VpRouter, + authorizationRequestExpirationInSeconds: 600, + federation: { + async getAuthorityHints(agentContext, { verifierId }) { + return getAuthorityHints(verifierTrustChains, verifierId).map((verifierId) => + joinUriParts(AGENT_HOST, ['oid4vp', verifierId]) + ) + }, + async isSubordinateEntity(agentContext, options) { + return isSubordinateTo(verifierTrustChains, options.verifierId, options.subjectEntityId).length > 0 + }, + }, + }), + x509: new X509Module({ + getTrustedCertificatesForVerification: (agentContext, { certificateChain }) => { + return [certificateChain[certificateChain.length - 1].toString('pem')] + }, + trustedCertificates: [ + x509PidIssuerCertificate, + x509BdrMdlIssuerCertificate, + x509PidIssuerRootCertificate, + certs.IACA_RDW_EU_PID, + certs.GOOGLE_CM_WALLET, + certs.IACA_Bosa, + certs.IACA_Luxembourg_TST, + certs.IACA_Clear, + certs.IACA_COI_Poland, + certs.IACA_Google, + certs.IACA_LT, + // secp521r1 is not supported + // certs.IACA_Google_mDL, + certs.IACA_Luxembourg_QUA, + certs.IACA_Panasonic, + certs.Intermediate_IACA_Luxembourg_QUA, + certs.IACA_Toppan_mDL, + certs.IACA_IPZS, + certs.IACA_Procivis, + certs.IACA_Veridos, + certs.IACA_OGCIO, + certs.IACA_Credence_ID, + certs.IACA_Zetes, + certs.IACA_OIDF, + certs.Intermediate_IACA_Luxembourg_TST, + certs.IACA_Nortal, + certs.IACA_SpruceID, + certs.IACA_SICPA, + certs.IACA_Idakto, + certs.IACA_RDW_mDL, + certs.IACA_Explicit_Selection, + certs.IACA_GRNET, + certs.IACA_Idemia, + certs.IACA_Apple, + certs.IACA_FeliCA, + certs.IACA_HID, + certs.IACA_Scytales, + certs.IACA_Bundesdruckerei, + certs.IACA_Animo_EU_PID, + certs.IACA_CLR_Labs_0, + certs.IACA_Animo_mDL, + certs.IACA_CLR_Labs_1, + certs.IACA_Reaktor, + certs.IACA_AMA, + certs.IACA_Samsung, + certs.IACA_France_Identite_Test, + certs.IACA_CLR_Labs_2, + ], }), }, -}); +}) diff --git a/agent/src/ai.ts b/agent/src/ai.ts new file mode 100644 index 0000000..c0cca0e --- /dev/null +++ b/agent/src/ai.ts @@ -0,0 +1,118 @@ +import { anthropic } from '@ai-sdk/anthropic' +import { generateObject } from 'ai' +import { z } from 'zod' + +export const zValidateVerificationRequestSchema = z.object({ + verifier: z.object({ + name: z.string(), + domain: z.string(), + }), + name: z.string(), + purpose: z.string(), + cards: z.array( + z.object({ + name: z.string(), + subtitle: z.string(), + requestedAttributes: z.array(z.string()), + }) + ), +}) + +const zResponseSchema = z.object({ + reason: z.string(), + validRequest: z.enum(['yes', 'no', 'could_not_determine']), +}) + +export const validateVerificationRequest = async ({ + verifier, + name, + purpose, + cards, +}: z.infer) => { + const rc = cards + .map( + (credential) => + `${credential.name} - ${credential.subtitle}. Requested attributes: ${credential.requestedAttributes.join(', ')}` + ) + .join('\n') + + const prompt = BASE_PROMPT(verifier, name, purpose, rc) + + // Can be improved by adding a reasoning step, but that makes it quite slow + const { object } = await generateObject({ + model: anthropic('claude-3-7-sonnet-20250219'), + schema: zResponseSchema, + prompt, + }).catch((e) => { + console.error(e) + return { object: { validRequest: 'could_not_determine', reason: 'AI request failed' } } + }) + + console.log('AI:::', JSON.stringify(object, null, 2)) + + return object +} + +const BASE_PROMPT = ( + verifier: { name: string; domain: string }, + requestName: string, + requestPurpose: string, + requestedCards: string +) => { + const hasRequestName = requestName !== 'No name provided' + + return ` +You are an AI assistant tasked with analyzing data verification requests to identify potential overasking of information. Your goal is to determine if the requested data is appropriate and necessary for the stated purpose of the request. + +You will be provided with the following information: + +Verifier Name: + +${verifier.name} + + +Verifier Domain: + +${verifier.domain} + + +${ + hasRequestName + ? ` +Request Name: + +${requestName} + +` + : '' +} + +Request Purpose: + +${requestPurpose} + + +Requested Cards: + +${requestedCards} + + +Analyze the request by following these steps: + +1. Carefully review the verifier ${hasRequestName ? 'and the request name' : ''} and purpose. +2. Examine each requested card, including its name, subtitle, and attributes. +3. For each piece of requested information, consider whether it is necessary and appropriate for the stated purpose. +4. Identify any instances of overasking of personal information, where the requested information exceeds what is reasonably required for the purpose. + +Guidelines for identifying overasking: +- Consider only if overasking of sensitive personal information is the case. Overasking of common info such as the name or portrait of the person, or metadata related to the card such as date of issuance or expiration should not be a reason to reject the request. +- Asking the same information from different cards should not be considered overasking. +- Assess if the requested information is excessive for the stated purpose. +- Determine if the information request aligns with common practices for similar purposes in the real world. +- If you cannot determine whether the request is overasking, respond with "could_not_determine". + +Briefly summarize whether the request appears appropriate or if there is evidence of overasking. Remember to be thorough in your analysis and provide clear, concise explanations for your assessments. If you find no evidence of overasking, state this clearly and explain why the requested information appears appropriate for the purpose. + +Keep your response concise and to the point. +` +} diff --git a/agent/src/constants.ts b/agent/src/constants.ts index b80ad39..5039a82 100644 --- a/agent/src/constants.ts +++ b/agent/src/constants.ts @@ -1,31 +1,27 @@ if ( - !process.env.ED25519_SEED || - !process.env.P256_SEED || + !process.env.ROOT_P256_SEED || + !process.env.DCS_P256_SEED || !process.env.AGENT_HOST || !process.env.AGENT_WALLET_KEY ) { - throw new Error( - "ED25519_SEED, P256_SEED, AGENT_HOST or AGENT_WALLET_KEY env variable not set" - ); + throw new Error('ROOT_P256_SEED, DCS_P256_SEED, AGENT_HOST or AGENT_WALLET_KEY env variable not set') } -const AGENT_HOST = process.env.AGENT_HOST; -const AGENT_WALLET_KEY = process.env.AGENT_WALLET_KEY; +const AGENT_HOST = process.env.AGENT_HOST +const AGENT_DNS = AGENT_HOST.replace('https://', '') +const AGENT_WALLET_KEY = process.env.AGENT_WALLET_KEY -const DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED = - process.env.DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED; - -const CHEQD_TESTNET_COSMOS_PAYER_SEED = - process.env.CHEQD_TESTNET_COSMOS_PAYER_SEED; - -const ED25519_SEED = process.env.ED25519_SEED; -const P256_SEED = process.env.P256_SEED; +const ROOT_P256_SEED = process.env.ROOT_P256_SEED +const DCS_P256_SEED = process.env.DCS_P256_SEED +const X509_ROOT_CERTIFICATE = process.env.X509_ROOT_CERTIFICATE +const X509_DCS_CERTIFICATE = process.env.X509_DCS_CERTIFICATE export { AGENT_HOST, AGENT_WALLET_KEY, - DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED, - CHEQD_TESTNET_COSMOS_PAYER_SEED, - ED25519_SEED, - P256_SEED, -}; + ROOT_P256_SEED, + DCS_P256_SEED, + X509_ROOT_CERTIFICATE, + X509_DCS_CERTIFICATE, + AGENT_DNS, +} diff --git a/agent/src/did/cheqd.ts b/agent/src/did/cheqd.ts deleted file mode 100644 index c01ebcd..0000000 --- a/agent/src/did/cheqd.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { agent } from "../agent"; -import { CheqdDidCreateOptions } from "@credo-ts/cheqd"; -import { - DidDocumentBuilder, - utils, - KeyType, - getEd25519VerificationKey2018, -} from "@credo-ts/core"; - -export async function createDidCheqd() { - // NOTE: we need to pass custom document for cheqd if we want to add it to `assertionMethod` - - const did = `did:cheqd:testnet:${utils.uuid()}`; - const key = await agent.wallet.createKey({ - keyType: KeyType.Ed25519, - }); - - const ed25519VerificationMethod = getEd25519VerificationKey2018({ - key, - id: `${did}#key-1`, - controller: did, - }); - - const didDocument = new DidDocumentBuilder(did) - .addContext("https://w3id.org/security/suites/ed25519-2018/v1") - .addVerificationMethod(ed25519VerificationMethod) - .addAssertionMethod(ed25519VerificationMethod.id) - .addAuthentication(ed25519VerificationMethod.id) - .build(); - - const didResult = await agent.dids.create({ - method: "cheqd", - didDocument, - options: { - network: "testnet", - }, - secret: {}, - }); - - if (didResult.didState.state === "failed") { - throw new Error("cheqd DID creation failed. " + didResult.didState.reason); - } - - return [did]; -} diff --git a/agent/src/did/createKeys.ts b/agent/src/did/createKeys.ts deleted file mode 100644 index f6ae599..0000000 --- a/agent/src/did/createKeys.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { KeyType, TypedArrayEncoder } from "@credo-ts/core"; -import { agent } from "../agent"; -import { ED25519_SEED, P256_SEED } from "../constants"; - -export async function createKeys() { - const ed25519Key = await agent.wallet.createKey({ - keyType: KeyType.Ed25519, - seed: TypedArrayEncoder.fromString(ED25519_SEED), - }); - - const p256Key = await agent.wallet.createKey({ - keyType: KeyType.P256, - seed: TypedArrayEncoder.fromString(P256_SEED), - }); - - return [ed25519Key, p256Key]; -} diff --git a/agent/src/did/index.ts b/agent/src/did/index.ts deleted file mode 100644 index ee1c8f2..0000000 --- a/agent/src/did/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./util"; -export * from "./web"; -export * from "./jwk"; -export * from "./key"; -export * from "./cheqd"; -export * from "./indy"; -export * from "./setup"; diff --git a/agent/src/did/indy.ts b/agent/src/did/indy.ts deleted file mode 100644 index 4765846..0000000 --- a/agent/src/did/indy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { KeyType, TypedArrayEncoder } from "@credo-ts/core"; -import { indyDidFromPublicKeyBase58 } from "@credo-ts/core/build/utils"; -import { agent } from "../agent"; - -export async function importIndyDid( - namespaceIdentifier: string, - privateKey: string -) { - const key = await agent.wallet.createKey({ - keyType: KeyType.Ed25519, - privateKey: TypedArrayEncoder.fromString(privateKey), - }); - - const indyDid = `did:indy:${namespaceIdentifier}:${indyDidFromPublicKeyBase58( - key.publicKeyBase58 - )}`; - - console.log({ - indyDid, - publicKeyBase58: key.publicKeyBase58, - }); - await agent.dids.import({ - did: indyDid, - overwrite: true, - }); -} diff --git a/agent/src/did/jwk.ts b/agent/src/did/jwk.ts deleted file mode 100644 index 7813846..0000000 --- a/agent/src/did/jwk.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { DidJwk, Key, getJwkFromKey } from "@credo-ts/core"; -import { agent } from "../agent"; - -export async function createDidJwk(keys: Key[]) { - const createdDids: string[] = []; - for (const key of keys) { - const didJwk = DidJwk.fromJwk(getJwkFromKey(key)); - await agent.dids.import({ - overwrite: true, - did: didJwk.did, - }); - createdDids.push(didJwk.did); - } - - return createdDids; -} diff --git a/agent/src/did/key.ts b/agent/src/did/key.ts deleted file mode 100644 index 6dbdfc2..0000000 --- a/agent/src/did/key.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Key, DidKey } from "@credo-ts/core"; -import { agent } from "../agent"; - -export async function createDidKey(keys: Key[]) { - const createdDids: string[] = []; - - for (const key of keys) { - const didKey = new DidKey(key); - await agent.dids.import({ - overwrite: true, - did: didKey.did, - }); - createdDids.push(didKey.did); - } - - return createdDids; -} diff --git a/agent/src/did/setup.ts b/agent/src/did/setup.ts deleted file mode 100644 index 73fbb6c..0000000 --- a/agent/src/did/setup.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { WalletKeyExistsError } from "@credo-ts/core"; -import { createDidCheqd } from "./cheqd"; -import { createKeys } from "./createKeys"; -import { createDidJwk } from "./jwk"; -import { createDidKey } from "./key"; -import { createDidWeb } from "./web"; -import { agent } from "../agent"; - -const availableDids: string[] = []; - -export async function setupAllDids() { - try { - const keys = await createKeys(); - - availableDids.push(...(await createDidKey(keys))); - availableDids.push(...(await createDidJwk(keys))); - availableDids.push(...(await createDidWeb(keys))); - availableDids.push(...(await createDidCheqd())); - - await agent.genericRecords.save({ - id: "AVAILABLE_DIDS", - content: { - availableDids, - }, - }); - } catch (error) { - // If the key already exists, we assume the dids are already created - if (error instanceof WalletKeyExistsError) { - const availableDidsRecord = await agent.genericRecords.findById( - "AVAILABLE_DIDS" - ); - if (!availableDidsRecord) { - throw new Error("No available dids record found"); - } - availableDids.push( - ...(availableDidsRecord.content.availableDids as string[]) - ); - - return; - } - - throw error; - } -} - -export function getAvailableDids() { - return availableDids; -} diff --git a/agent/src/did/util.ts b/agent/src/did/util.ts deleted file mode 100644 index 1d7638d..0000000 --- a/agent/src/did/util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { agent } from "../agent"; - -export async function hasDidForMethod(method: string) { - const [createdDid] = await agent.dids.getCreatedDids({ method }); - - return createdDid !== undefined; -} - -export async function getDidForMethod(method: string) { - const [createdDid] = await agent.dids.getCreatedDids({ method }); - - if (!createdDid) { - throw new Error(`did for method ${method} does not exist`); - } - - return createdDid.did; -} diff --git a/agent/src/did/web.ts b/agent/src/did/web.ts deleted file mode 100644 index 959c692..0000000 --- a/agent/src/did/web.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - DidDocumentBuilder, - Key, - getKeyDidMappingByKeyType, -} from "@credo-ts/core"; -import { agent } from "../agent"; -import { AGENT_HOST } from "../constants"; - -const cleanHost = encodeURIComponent( - AGENT_HOST.replace("https://", "").replace("http://", "") -); - -const didWeb = `did:web:${cleanHost}`; - -export async function createDidWeb(keys: Key[]) { - const verificationMethods = keys.flatMap((key) => - getKeyDidMappingByKeyType(key.keyType).getVerificationMethods(didWeb, key) - ); - - const didDocumentBuilder = new DidDocumentBuilder(didWeb); - - for (const verificationMethod of verificationMethods) { - didDocumentBuilder - .addVerificationMethod(verificationMethod) - .addAssertionMethod(verificationMethod.id) - .addAuthentication(verificationMethod.id); - } - - const didDocument = didDocumentBuilder.build(); - - await agent.dids.import({ - did: didWeb, - didDocument, - }); - - return [didWeb]; -} - -export async function getWebDidDocument() { - const [createdDid] = await agent.dids.getCreatedDids({ did: didWeb }); - - if (!createdDid || !createdDid.didDocument) { - throw new Error(`did does not exist`); - } - - return createdDid.didDocument; -} diff --git a/agent/src/didWeb.ts b/agent/src/didWeb.ts new file mode 100644 index 0000000..dc2d875 --- /dev/null +++ b/agent/src/didWeb.ts @@ -0,0 +1,47 @@ +import { + DidDocumentBuilder, + type Key, + VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, + getEd25519VerificationKey2020, +} from '@credo-ts/core' +import { agent } from './agent' +import { AGENT_HOST } from './constants' + +const cleanHost = encodeURIComponent(AGENT_HOST.replace('https://', '').replace('http://', '')) + +const didWeb = `did:web:${cleanHost}` + +export async function createDidWeb(key: Key) { + const didDocumentBuilder = new DidDocumentBuilder(didWeb) + + const verificationMethod = getEd25519VerificationKey2020({ + key, + id: `${didWeb}#key-1`, + controller: didWeb, + }) + + didDocumentBuilder + .addContext('https://w3id.org/security/suites/ed25519-2020/v1') + .addVerificationMethod(verificationMethod) + .addAssertionMethod(verificationMethod.id) + .addAuthentication(verificationMethod.id) + + const didDocument = didDocumentBuilder.build() + + await agent.dids.import({ + did: didWeb, + didDocument, + }) + + return [didWeb] +} + +export async function getWebDidDocument() { + const [createdDid] = await agent.dids.getCreatedDids({ did: didWeb }) + + if (!createdDid || !createdDid.didDocument) { + throw new Error('did does not exist') + } + + return createdDid.didDocument +} diff --git a/agent/src/endpoints.ts b/agent/src/endpoints.ts index c8321be..1af77fe 100644 --- a/agent/src/endpoints.ts +++ b/agent/src/endpoints.ts @@ -1,348 +1,538 @@ -import { agent } from "./agent"; -import express, { NextFunction, Request, Response } from "express"; -import z from "zod"; -import { credentialSupportedIds } from "./issuerMetadata"; -import { getIssuer } from "./issuer"; +import { randomUUID } from 'crypto' import { - DifPresentationExchangeService, + JsonEncoder, JsonTransformer, - KeyDidCreateOptions, - KeyType, + Jwt, + MdocDeviceResponse, RecordNotFoundError, - W3cJsonLdVerifiableCredential, + TypedArrayEncoder, W3cJsonLdVerifiablePresentation, - W3cJwtVerifiableCredential, W3cJwtVerifiablePresentation, - getJwkFromKey, - getKeyFromVerificationMethod, - parseDid, -} from "@credo-ts/core"; -import { getAvailableDids, getWebDidDocument } from "./did"; -import { getVerifier } from "./verifier"; -import { OpenId4VcIssuanceSessionRepository } from "@credo-ts/openid4vc/build/openid4vc-issuer/repository"; -import { OfferSessionMetadata } from "./session"; -import { OpenId4VcVerificationSessionState } from "@credo-ts/openid4vc"; + X509Certificate, + X509ModuleConfig, +} from '@credo-ts/core' +import { type OpenId4VcVerificationSessionRecord, OpenId4VcVerificationSessionState } from '@credo-ts/openid4vc' +import express, { type NextFunction, type Request, type Response } from 'express' +import z from 'zod' +import { agent } from './agent' +import { validateVerificationRequest, zValidateVerificationRequestSchema } from './ai' +import { + funkeDeployedAccessCertificate, + funkeDeployedAccessCertificateRoot, + funkeDeployedRegistrationCertificate, +} from './eudiTrust' +import { getIssuerIdForCredentialConfigurationId } from './issuer' +import { issuers } from './issuers' +import { getX509DcsCertificate, getX509RootCertificate } from './keyMethods' +import { oidcUrl } from './oidcProvider/provider' +import { LimitedSizeCollection } from './utils/LimitedSizeCollection' +import { type PlaygroundVerifierOptions, getVerifier } from './verifier' +import { verifiers } from './verifiers' +import { dcqlQueryFromRequest, presentationDefinitionFromRequest } from './verifiers/util' + +const responseCodeMap = new LimitedSizeCollection() const zCreateOfferRequest = z.object({ - // FIXME: rename offeredCredentials to credentialSupportedIds in AFJ - credentialSupportedIds: z.array(z.enum(credentialSupportedIds)), - issuerDidMethod: z.string(), -}); - -export const apiRouter = express.Router(); -apiRouter.use(express.json()); -apiRouter.use(express.text()); -apiRouter.post( - "/offers/create", - async (request: Request, response: Response) => { - const issuer = await getIssuer(); - // FIXME: somehow JSON doesn't work - const createOfferRequest = zCreateOfferRequest.parse( - typeof request.body === "string" ? JSON.parse(request.body) : request.body - ); - - const offer = await agent.modules.openId4VcIssuer.createCredentialOffer({ - issuerId: issuer.issuerId, - offeredCredentials: createOfferRequest.credentialSupportedIds, - preAuthorizedCodeFlowConfig: { - userPinRequired: false, - }, - }); - - // FIXME: in 0.5.1 we can pass the issuanceMetadata to the createCredentialOffer method - // directly - const issuanceSessionRepository = agent.dependencyManager.resolve( - OpenId4VcIssuanceSessionRepository - ); - offer.issuanceSession.issuanceMetadata = { - issuerDidMethod: createOfferRequest.issuerDidMethod, - } satisfies OfferSessionMetadata; - await issuanceSessionRepository.update( - agent.context, - offer.issuanceSession - ); - - return response.json(offer); - } -); + credentialSupportedIds: z.array(z.string()), + authorization: z.enum(['pin', 'none', 'presentation', 'browser']).default('none'), + requireDpop: z.boolean().default(false), + requireWalletAttestation: z.boolean().default(false), + requireKeyAttestation: z.boolean().default(false), +}) + +const zAddX509CertificateRequest = z.object({ + certificate: z.string(), +}) + +export const apiRouter = express.Router() + +apiRouter.use(express.json()) +apiRouter.use(express.text()) +apiRouter.post('/offers/create', async (request: Request, response: Response) => { + const createOfferRequest = zCreateOfferRequest.parse(request.body) + + // TODO: support multiple credential isuance + const configurationId = createOfferRequest.requireKeyAttestation + ? `${createOfferRequest.credentialSupportedIds[0]}-key-attestations` + : createOfferRequest.credentialSupportedIds[0] + + const issuerId = getIssuerIdForCredentialConfigurationId(configurationId) + const authorization = createOfferRequest.authorization + const issuerMetadata = await agent.modules.openId4VcIssuer.getIssuerMetadata(issuerId) + + const offer = await agent.modules.openId4VcIssuer.createCredentialOffer({ + issuerId, + credentialConfigurationIds: [configurationId], + version: 'v1.draft15', + authorization: { + requireDpop: createOfferRequest.requireDpop, + requireWalletAttestation: createOfferRequest.requireWalletAttestation, + }, + preAuthorizedCodeFlowConfig: + authorization === 'pin' || authorization === 'none' + ? { + authorizationServerUrl: issuerMetadata.credentialIssuer.credential_issuer, + txCode: + authorization === 'pin' + ? { + input_mode: 'numeric', + length: 4, + } + : undefined, + } + : undefined, + authorizationCodeFlowConfig: + authorization === 'browser' || authorization === 'presentation' + ? { + requirePresentationDuringIssuance: authorization === 'presentation', + authorizationServerUrl: + authorization === 'browser' ? oidcUrl : issuerMetadata.credentialIssuer.credential_issuer, + } + : undefined, + }) -apiRouter.get("/issuer", async (_, response: Response) => { - const issuer = await getIssuer(); - const availableDids = getAvailableDids(); + return response.json(offer) +}) +apiRouter.get('/x509', async (_, response: Response) => { + const certificate = getX509RootCertificate() + const instance = X509Certificate.fromEncodedCertificate(certificate) return response.json({ - credentialsSupported: issuer.credentialsSupported, - display: issuer.display, - availableDidMethods: Array.from( - new Set(availableDids.map((did) => `did:${parseDid(did).method}`)) - ), - }); -}); - -const zReceiveOfferRequest = z.object({ - credentialOfferUri: z.string().url(), -}); - -apiRouter.post( - "/offers/receive", - async (request: Request, response: Response) => { - const receiveOfferRequest = zReceiveOfferRequest.parse( - typeof request.body === "string" ? JSON.parse(request.body) : request.body - ); - - const resolvedOffer = - await agent.modules.openId4VcHolder.resolveCredentialOffer( - receiveOfferRequest.credentialOfferUri - ); - const credentials = - await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedOffer, - { - verifyCredentialStatus: false, - credentialBindingResolver: async ({ - keyType, - supportsJwk, - supportedDidMethods, - }) => { - if (supportedDidMethods?.includes("did:key")) { - const didKeyResult = await agent.dids.create( - { - method: "key", - options: { - keyType, - }, - } - ); - - if (didKeyResult.didState.state !== "finished") { - throw new Error("did creation failed"); - } - const firstVerificationMethod = - didKeyResult.didState.didDocument.verificationMethod?.[0]; - if (!firstVerificationMethod) { - throw new Error("did document has no verification method"); - } + base64: instance.toString('base64'), + pem: instance.toString('pem'), + decoded: instance.toString('text'), + }) +}) - return { - method: "did", - didUrl: firstVerificationMethod.id, - }; - } +apiRouter.post('/x509', async (request: Request, response: Response) => { + const addX509CertificateRequest = zAddX509CertificateRequest.parse(request.body) - if (supportsJwk) { - const key = await agent.wallet.createKey({ - keyType, - }); + const trustedCertificates = agent.dependencyManager.resolve(X509ModuleConfig).trustedCertificates + try { + const instance = X509Certificate.fromEncodedCertificate(addX509CertificateRequest.certificate) + const base64 = instance.toString('base64') - return { - method: "jwk", - jwk: getJwkFromKey(key), - }; - } + if (!trustedCertificates?.includes(base64)) { + agent.x509.addTrustedCertificate(base64) + } - throw new Error( - "only did:key and jwk are supported for holder binding" - ); - }, - } - ); + return response.send(instance.toString('text')) + } catch (error) { + return response.status(500).json({ + errorMessage: 'error adding x509 certificate', + error, + }) + } +}) - return response.json({ - credentials: credentials.map((credential) => { - if (credential instanceof W3cJsonLdVerifiableCredential) { - return { - pretty: credential.toJson(), - encoded: credential.toJson(), - }; - } +apiRouter.get('/issuers', async (_, response: Response) => { + return response.json( + issuers.map((issuer) => { + return { + id: issuer.issuerId, + name: issuer.playgroundDisplayName ?? issuer.display[0].name, + tags: issuer.tags, + logo: issuer.display[0].logo.uri, + + credentials: Object.values(issuer.credentialConfigurationsSupported).map((values) => { + const first = Object.values(values)[0] - if (credential instanceof W3cJwtVerifiableCredential) { return { - pretty: JsonTransformer.toJSON(credential.credential), - encoded: credential.serializedJwt, - }; - } + display: first.configuration.display[0], + formats: Object.fromEntries( + Object.entries(values).flatMap(([format, configuration]) => [ + [format, configuration.data.credentialConfigurationId], + ...(format === 'vc+sd-jwt' + ? [['dc+sd-jwt', `${configuration.data.credentialConfigurationId}-dc-sd-jwt`]] + : []), + ]) + ), + } + }), + } + }) + ) +}) - return { - pretty: { - ...credential, - compact: undefined, - }, - encoded: credential.compact, - }; - }), - }); +apiRouter.get('/verifier', async (_, response: Response) => { + return response.json({ + presentationRequests: verifiers.flatMap((verifier) => + verifier.requests.map((c, index) => ({ + useCase: 'useCase' in verifier ? verifier.useCase : undefined, + display: c.name, + id: `${verifier.verifierId}__${index}`, + })) + ), + }) +}) + +apiRouter.post('/trust-chains', async (request: Request, response: Response) => { + const parseResult = await z + .object({ + entityId: z.string(), + trustAnchorEntityIds: z.array(z.string()).nonempty(), + }) + .safeParseAsync(request.body) + + if (!parseResult.success) { + return response.status(400).json({ + error: parseResult.error.message, + details: parseResult.error.issues, + }) } -); + + const { entityId, trustAnchorEntityIds } = parseResult.data + + const chains = await agent.modules.openId4VcHolder.resolveOpenIdFederationChains({ + entityId, + trustAnchorEntityIds, + }) + + return response.json(chains) +}) const zCreatePresentationRequestBody = z.object({ - presentationDefinition: z.record(z.string(), z.any()), -}); - -apiRouter.post( - "/requests/create", - async (request: Request, response: Response) => { - const verifier = await getVerifier(); - - // FIXME: somehow JSON doesn't work - const createPresentationRequestBody = zCreatePresentationRequestBody.parse( - typeof request.body === "string" ? JSON.parse(request.body) : request.body - ); - - // DIIPv1 uses ES256/P256 keys so we use that to create the request - const webDid = await getWebDidDocument(); - const verificationMethods = webDid.verificationMethod ?? []; - const keys = verificationMethods.map((v) => - getKeyFromVerificationMethod(v) - ); - const verificationMethodIndex = keys.findIndex( - (k) => k.keyType === KeyType.P256 - ); - - if (verificationMethodIndex === -1) { - return response.status(500).json({ - error: "No P-256 verification method found", - }); + requestSignerType: z.enum(['none', 'x5c', 'openid-federation']), + presentationDefinitionId: z.string(), + requestScheme: z.string(), + responseMode: z.enum(['direct_post.jwt', 'direct_post', 'dc_api', 'dc_api.jwt']), + purpose: z.string().optional(), + transactionAuthorizationType: z.enum(['none', 'qes']), + version: z.enum(['v1.draft21', 'v1.draft24']).default('v1.draft24'), + queryLanguage: z.enum(['pex', 'dcql']).default('dcql'), + redirectUriBase: z.string().url().optional(), +}) + +const zReceiveDcResponseBody = z.object({ + verificationSessionId: z.string(), + data: z.union([z.string(), z.record(z.unknown())]), +}) + +apiRouter.post('/requests/create', async (request: Request, response: Response) => { + try { + const { + requestSignerType, + transactionAuthorizationType, + presentationDefinitionId, + requestScheme, + responseMode, + version, + purpose, + queryLanguage, + redirectUriBase, + } = await zCreatePresentationRequestBody.parseAsync(request.body) + + const x509RootCertificate = getX509RootCertificate() + const x509DcsCertificate = getX509DcsCertificate() + + const [verifierId, requestIndex] = presentationDefinitionId.split('__') + const verifier = await getVerifier(verifierId) + + // biome-ignore lint/suspicious/noExplicitAny: + const definition = (verifiers.find((v) => v.verifierId === verifierId)?.requests as any)[ + requestIndex + ] as PlaygroundVerifierOptions['requests'][number] + if (!definition) { + return response.status(404).json({ + error: 'Definition not found', + }) } - const { authorizationRequest, verificationSession } = + console.log('Requesting definition', JSON.stringify(definition, null, 2)) + + const queryLanguageDefinition = + queryLanguage === 'pex' + ? presentationDefinitionFromRequest(definition, purpose) + : dcqlQueryFromRequest(definition, purpose) + + const credentialIds = + 'input_descriptors' in queryLanguageDefinition + ? queryLanguageDefinition.input_descriptors.map((descriptor) => descriptor.id) + : queryLanguageDefinition.credentials.map((query) => query.id) + + const responseCode = randomUUID() + const redirectUri = redirectUriBase ? `${redirectUriBase}?response_code=${responseCode}` : undefined + + // Only include it in this one + const isEudiAuthorization = presentationDefinitionId === '044721ed-af79-45ec-bab3-de85c3e722d0__1' + const { authorizationRequest, verificationSession, authorizationRequestObject } = await agent.modules.openId4VcVerifier.createAuthorizationRequest({ + authorizationResponseRedirectUri: redirectUri, verifierId: verifier.verifierId, - requestSigner: { - didUrl: verificationMethods[verificationMethodIndex].id, - method: "did", - }, - presentationExchange: { - definition: - createPresentationRequestBody.presentationDefinition as any, - }, - }); + // @ts-ignore + additionalJwtPayload: isEudiAuthorization + ? { + verifier_attestations: [ + { + format: 'jwt', + data: funkeDeployedRegistrationCertificate, + }, + ], + } + : undefined, + requestSigner: + requestSignerType === 'none' + ? { method: 'none' } + : requestSignerType === 'x5c' + ? // Include the certificate from the german registrar + isEudiAuthorization + ? { + method: 'x5c', + x5c: [ + X509Certificate.fromEncodedCertificate(funkeDeployedAccessCertificate).toString('base64'), + X509Certificate.fromEncodedCertificate(funkeDeployedAccessCertificateRoot).toString('base64'), + ], + } + : { + method: 'x5c', + x5c: [x509DcsCertificate, x509RootCertificate], + } + : { + method: 'federation', + }, + + transactionData: + transactionAuthorizationType === 'qes' + ? [ + { + credential_ids: credentialIds as [string, ...string[]], + type: 'qes_authorization', + transaction_data_hashes_alg: ['sha-256'], + signatureQualifier: 'eu_eidas_qes', + documentDigests: [ + { + hash: 'some-hash', + label: 'Declaration of Independence.pdf', + hashAlgorithmOID: 'something', + }, + ], + }, + ] + : undefined, + presentationExchange: + 'input_descriptors' in queryLanguageDefinition + ? { + definition: queryLanguageDefinition, + } + : undefined, + dcql: + 'credentials' in queryLanguageDefinition + ? { + query: queryLanguageDefinition, + } + : undefined, + responseMode, + version, + expectedOrigins: + requestSignerType !== 'none' && responseMode.includes('dc_api') + ? [request.headers.origin as string] + : undefined, + }) + + if (redirectUri) { + responseCodeMap.set(responseCode, verificationSession.id) + } + + const authorizationRequestJwt = verificationSession.authorizationRequestJwt + ? Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt) + : undefined + const authorizationRequestPayload = verificationSession.requestPayload + const dcqlQuery = authorizationRequestPayload.dcql_query + const presentationDefinition = authorizationRequestPayload.presentation_definition + const transactionData = authorizationRequestPayload.transaction_data?.map((e) => JsonEncoder.fromBase64(e)) + console.log(JSON.stringify(authorizationRequestObject, null, 2)) return response.json({ - authorizationRequestUri: authorizationRequest, + authorizationRequestObject, + authorizationRequestUri: authorizationRequest.replace('openid4vp://', requestScheme), verificationSessionId: verificationSession.id, - }); + responseStatus: verificationSession.state, + dcqlQuery, + definition: presentationDefinition, + transactionData, + authorizationRequest: authorizationRequestJwt + ? { + payload: authorizationRequestJwt.payload.toJson(), + header: authorizationRequestJwt.header, + } + : authorizationRequestPayload, + }) + } catch (error) { + return response.status(400).json({ + message: error instanceof Error ? error.message : 'Unknown error occurred', + }) } -); +}) -const zReceivePresentationRequestBody = z.object({ - authorizationRequestUri: z.string().url(), -}); +async function getVerificationStatus(verificationSession: OpenId4VcVerificationSessionRecord) { + const authorizationRequestJwt = verificationSession.authorizationRequestJwt + ? Jwt.fromSerializedJwt(verificationSession.authorizationRequestJwt) + : undefined + const authorizationRequestPayload = verificationSession.requestPayload -apiRouter.get("/requests/:verificationSessionId", async (request, response) => { - const verificationSessionId = request.params.verificationSessionId; + const authorizationRequest = { + payload: verificationSession.requestPayload, + header: authorizationRequestJwt?.header, + } + const dcqlQuery = authorizationRequestPayload.dcql_query + const presentationDefinition = authorizationRequestPayload.presentation_definition - try { - const verificationSession = - await agent.modules.openId4VcVerifier.getVerificationSessionById( - verificationSessionId - ); - - if ( - verificationSession.state === - OpenId4VcVerificationSessionState.ResponseVerified - ) { - const verified = - await agent.modules.openId4VcVerifier.getVerifiedAuthorizationResponse( - verificationSessionId - ); - - return response.json({ - verificationSessionId: verificationSession.id, - responseStatus: verificationSession.state, - error: verificationSession.errorMessage, - - presentations: verified.presentationExchange?.presentations.map( - (presentation) => { - if (presentation instanceof W3cJsonLdVerifiablePresentation) { - return { - pretty: presentation.toJson(), - encoded: presentation.toJson(), - }; + const transactionData = authorizationRequestPayload.transaction_data?.map((e) => JsonEncoder.fromBase64(e)) + + if (verificationSession.state === OpenId4VcVerificationSessionState.ResponseVerified) { + const verified = await agent.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(verificationSession.id) + console.log(verified.presentationExchange?.presentations) + console.log(verified.dcql?.presentationResult) + + const presentations = await Promise.all( + (verified.presentationExchange?.presentations ?? Object.values(verified.dcql?.presentations ?? {})).map( + async (presentation) => { + if (presentation instanceof W3cJsonLdVerifiablePresentation) { + return { + pretty: presentation.toJson(), + encoded: presentation.toJson(), } + } - if (presentation instanceof W3cJwtVerifiablePresentation) { - return { - pretty: JsonTransformer.toJSON(presentation.presentation), - encoded: presentation.serializedJwt, - }; + if (presentation instanceof W3cJwtVerifiablePresentation) { + return { + pretty: JsonTransformer.toJSON(presentation.presentation), + encoded: presentation.serializedJwt, } + } + if (presentation instanceof MdocDeviceResponse) { return { - pretty: { - ...presentation, - compact: undefined, - }, - encoded: presentation.compact, - }; + pretty: JsonTransformer.toJSON({ + documents: presentation.documents.map((doc) => ({ + doctype: doc.docType, + alg: doc.alg, + base64Url: doc.base64Url, + validityInfo: doc.validityInfo, + deviceSignedNamespaces: doc.deviceSignedNamespaces, + issuerSignedNamespaces: Object.entries(doc.issuerSignedNamespaces).map( + ([nameSpace, nameSpacEntries]) => [ + nameSpace, + Object.entries(nameSpacEntries).map(([key, value]) => + value instanceof Uint8Array + ? [`base64:${key}`, `data:image/jpeg;base64,${TypedArrayEncoder.toBase64(value)}`] + : [key, value] + ), + ] + ), + })), + }), + encoded: presentation.base64Url, + } } - ), - submission: verified.presentationExchange?.submission, - definition: verified.presentationExchange?.definition, - }); - } - return response.json({ + return { + pretty: { + ...presentation, + compact: undefined, + }, + encoded: presentation.compact, + } + } + ) ?? [] + ) + + const dcqlSubmission = verified.dcql + ? Object.keys(verified.dcql.presentations).map((key, index) => ({ + queryCredentialId: key, + presentationIndex: index, + })) + : undefined + + console.log('presentations', presentations) + + return { verificationSessionId: verificationSession.id, responseStatus: verificationSession.state, error: verificationSession.errorMessage, - }); + authorizationRequest, + + presentations: presentations, + + submission: verified.presentationExchange?.submission, + definition: verified.presentationExchange?.definition, + transactionDataSubmission: verified.transactionData, + + dcqlQuery, + dcqlSubmission: verified.dcql + ? { ...verified.dcql.presentationResult, vpTokenMapping: dcqlSubmission } + : undefined, + } + } + + return { + verificationSessionId: verificationSession.id, + responseStatus: verificationSession.state, + error: verificationSession.errorMessage, + authorizationRequest, + definition: presentationDefinition, + transactionData, + dcqlQuery, + } +} + +apiRouter.post('/requests/verify-dc', async (request: Request, response: Response) => { + const { verificationSessionId, data } = await zReceiveDcResponseBody.parseAsync(request.body) + + try { + const { verificationSession } = await agent.modules.openId4VcVerifier.verifyAuthorizationResponse({ + verificationSessionId, + authorizationResponse: typeof data === 'string' ? JSON.parse(data) : data, + origin: request.headers.origin, + }) + + return response.json(await getVerificationStatus(verificationSession)) } catch (error) { if (error instanceof RecordNotFoundError) { - return response.status(404).send("Verification session not found"); + return response.status(404).send('Verification session not found') } + return response.status(500).send({ error: error instanceof Error ? error.message : 'Unknown error' }) } -}); - -apiRouter.post( - "/requests/receive", - async (request: Request, response: Response) => { - const receivePresentationRequestBody = - zReceivePresentationRequestBody.parse( - typeof request.body === "string" - ? JSON.parse(request.body) - : request.body - ); - - const resolved = - await agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( - receivePresentationRequestBody.authorizationRequestUri - ); - - if (!resolved.presentationExchange) { - return response.status(500).json({ - error: - "Expected presentation_definition to be included in authorization request", - }); - } +}) - // FIXME: expose PresentationExchange API (or allow auto-select in another way) - const presentationExchangeService = agent.dependencyManager.resolve( - DifPresentationExchangeService - ); - - const selectedCredentials = - presentationExchangeService.selectCredentialsForRequest( - resolved.presentationExchange?.credentialsForRequest - ); - - const { submittedResponse, serverResponse } = - await agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ - authorizationRequest: resolved.authorizationRequest, - presentationExchange: { - credentials: selectedCredentials, - }, - }); - - return response.status(serverResponse.status).json(submittedResponse); +apiRouter.get('/requests/:verificationSessionId', async (request, response) => { + const verificationSessionId = + responseCodeMap.get(request.params.verificationSessionId) ?? request.params.verificationSessionId + + try { + const verificationSession = await agent.modules.openId4VcVerifier.getVerificationSessionById(verificationSessionId) + return response.json(await getVerificationStatus(verificationSession)) + } catch (error) { + if (error instanceof RecordNotFoundError) { + return response.status(404).send('Verification session not found') + } } -); +}) + +apiRouter.use((error: Error, _request: Request, response: Response, _next: NextFunction) => { + console.error('Unhandled error', error) + return response.status(500).json({ + error: error.message, + }) +}) + +apiRouter.post('/validate-verification-request', async (request: Request, response: Response) => { + try { + const validateVerificationRequestBody = zValidateVerificationRequestSchema.parse(request.body) + const result = await validateVerificationRequest(validateVerificationRequestBody) + return response.json(result) + } catch (error) { + if (error instanceof z.ZodError) { + return response.status(400).json({ + error: 'Invalid request body', + details: error.errors, + }) + } -apiRouter.use( - (error: Error, request: Request, response: Response, next: NextFunction) => { - console.error("Unhandled error", error); + console.error('Error validating verification request:', error) return response.status(500).json({ - error: error.message, - }); + error: 'Internal server error during verification validation', + message: error instanceof Error ? error.message : 'Unknown error', + }) } -); +}) diff --git a/agent/src/eudiTrust.ts b/agent/src/eudiTrust.ts new file mode 100644 index 0000000..7390d08 --- /dev/null +++ b/agent/src/eudiTrust.ts @@ -0,0 +1,31 @@ +export const funkeDeployedAccessCertificate = `-----BEGIN CERTIFICATE----- +MIICZzCCAg2gAwIBAgIUHj8qsBiHUZvnEpGep978NYQNjqkwCgYIKoZIzj0EAwIw +GzEZMBcGA1UEAwwQR2VybWFuIFJlZ2lzdHJhcjAeFw0yNTA1MTUxMTAwNDdaFw0y +NjA1MTUxMTAwNDdaMIGMMYGJMBYGA1UEYQwPREU6VFgtMTIzNDU4Nzg5MBcGA1UE +YQwQRUk6U0UtNTU2Nzk2MTQzMzAYBgNVBGEMEUVPUklERS0xMzM0NTY3ODkwMBkG +A1UEAwwSUmVseWluZyBQYXJ0eSBOYW1lMCEGA1UEYQwaTEVJWEctNTI5OTAxVDhC +TTQ5QVVSU0RPNTUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQVdU0DSVpr7k/a +eTk8kWi/zGEEU3PlQ4kEhWzKTRIpJ+pN8owHpH5wUM2KN2YFjK4RpSAyC5RzyE5W +eet5b22Eo4G8MIG5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoG +CCsGAQUFBwMBMBkGA1UdEQQSMBCCDmZ1bmtlLmFuaW1vLmlkMC8GA1UdHwQoMCYw +JKAioCCGHmh0dHBzOi8vZnVua2Utd2FsbGV0LmRlL2NhL2NybDAdBgNVHQ4EFgQU +YR8vFQTlkjf1/NnKeZxvY0Zz3aAwHwYDVR0jBBgwFoAUzGcouQaJ9tMqvFsZdwVc +rpEVB3cwCgYIKoZIzj0EAwIDSAAwRQIgE3WBsOwr5dCwN6OfZbExipJCOdQfJl14 +qsj55KChGGcCIQCb+CRdLsUHIlM0bxs2g7bmOFs3ZczQ5HaBCsdY4yPqTA== +-----END CERTIFICATE----- +` + +export const funkeDeployedAccessCertificateRoot = `-----BEGIN CERTIFICATE----- +MIIBdTCCARugAwIBAgIUHsSmbGuWAVZVXjqoidqAVClGx4YwCgYIKoZIzj0EAwIw +GzEZMBcGA1UEAwwQR2VybWFuIFJlZ2lzdHJhcjAeFw0yNTAzMzAxOTU4NTFaFw0y +NjAzMzAxOTU4NTFaMBsxGTAXBgNVBAMMEEdlcm1hbiBSZWdpc3RyYXIwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAASQWCESFd0Ywm9sK87XxqxDP4wOAadEKgcZFVX7 +npe3ALFkbjsXYZJsTGhVp0+B5ZtUao2NsyzJCKznPwTz2wJcoz0wOzAaBgNVHREE +EzARgg9mdW5rZS13YWxsZXQuZGUwHQYDVR0OBBYEFMxnKLkGifbTKrxbGXcFXK6R +FQd3MAoGCCqGSM49BAMCA0gAMEUCIQD4RiLJeuVDrEHSvkPiPfBvMxAXRC6PuExo +pUGCFdfNLQIgHGSa5u5ZqUtCrnMiaEageO71rjzBlov0YUH4+6ELioY= +-----END CERTIFICATE----- +` + +export const funkeDeployedRegistrationCertificate = + 'eyJ0eXAiOiJyYy1ycCtqd3QiLCJ4NWMiOlsiTUlJQmRUQ0NBUnVnQXdJQkFnSVVIc1NtYkd1V0FWWlZYanFvaWRxQVZDbEd4NFl3Q2dZSUtvWkl6ajBFQXdJd0d6RVpNQmNHQTFVRUF3d1FSMlZ5YldGdUlGSmxaMmx6ZEhKaGNqQWVGdzB5TlRBek16QXhPVFU0TlRGYUZ3MHlOakF6TXpBeE9UVTROVEZhTUJzeEdUQVhCZ05WQkFNTUVFZGxjbTFoYmlCU1pXZHBjM1J5WVhJd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFTUVdDRVNGZDBZd205c0s4N1h4cXhEUDR3T0FhZEVLZ2NaRlZYN25wZTNBTEZrYmpzWFlaSnNUR2hWcDArQjVadFVhbzJOc3l6SkNLem5Qd1R6MndKY296MHdPekFhQmdOVkhSRUVFekFSZ2c5bWRXNXJaUzEzWVd4c1pYUXVaR1V3SFFZRFZSME9CQllFRk14bktMa0dpZmJUS3J4YkdYY0ZYSzZSRlFkM01Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRRDRSaUxKZXVWRHJFSFN2a1BpUGZCdk14QVhSQzZQdUV4b3BVR0NGZGZOTFFJZ0hHU2E1dTVacVV0Q3JuTWlhRWFnZU83MXJqekJsb3YwWVVINCs2RUxpb1k9Il0sImFsZyI6IkVTMjU2In0.eyJwcml2YWN5X3BvbGljeSI6Imh0dHBzOi8vcGFyYWR5bS5pZC93YWxsZXQtcHJpdmFjeS1wb2xpY3kiLCJwdXJwb3NlIjpbeyJsb2NhbGUiOiJkZS1ERSIsIm5hbWUiOiJVbSBlaW4gS29udG8gYmVpIERlaW5lQmFuayB6dSBlcsO2ZmZuZW4sIG3DvHNzZW4gd2lyIElocmVuIE5hbWVuLCBJaHIgR2VidXJ0c2RhdHVtLCBJaHJlbiBXb2huc2l0eiB1bmQgSWhyZSBTdGFhdHNhbmdlaMO2cmlna2VpdCDDvGJlcnByw7xmZW4uIn1dLCJjb250YWN0Ijp7IndlYnNpdGUiOiJodHRwczovL3BhcmFkeW0uaWQiLCJlLW1haWwiOiJjb250YWN0QHBhcmFkeW0uaWQiLCJwaG9uZSI6IisxMjM0NTY3ODkwIn0sImNyZWRlbnRpYWxzIjpbeyJpZCI6IjAiLCJmb3JtYXQiOiJkYytzZC1qd3QiLCJtZXRhIjp7InZjdF92YWx1ZXMiOlsiaHR0cHM6Ly9kZW1vLnBpZC1pc3N1ZXIuYnVuZGVzZHJ1Y2tlcmVpLmRlL2NyZWRlbnRpYWxzL3BpZC8xLjAiXX0sImNsYWltcyI6W3sicGF0aCI6WyJnaXZlbl9uYW1lIl0sImlkIjoiZ2l2ZW5fbmFtZSJ9LHsicGF0aCI6WyJmYW1pbHlfbmFtZSJdLCJpZCI6ImZhbWlseV9uYW1lIn0seyJwYXRoIjpbImJpcnRoX2ZhbWlseV9uYW1lIl0sImlkIjoiYmlydGhfZmFtaWx5X25hbWUifSx7InBhdGgiOlsiYmlydGhkYXRlIl0sImlkIjoiYmlydGhkYXRlIn0seyJwYXRoIjpbImFnZV9lcXVhbF9vcl9vdmVyIiwiMTgiXSwiaWQiOiJhZ2VfZXF1YWxfb3Jfb3Zlcl8xOCJ9LHsicGF0aCI6WyJwbGFjZV9vZl9iaXJ0aCIsImxvY2FsaXR5Il0sImlkIjoicGxhY2Vfb2ZfYmlydGhfbG9jYWxpdHkifSx7InBhdGgiOlsiYWRkcmVzcyIsImxvY2FsaXR5Il0sImlkIjoiYWRkcmVzc19sb2NhbGl0eSJ9LHsicGF0aCI6WyJhZGRyZXNzIiwicG9zdGFsX2NvZGUiXSwiaWQiOiJhZGRyZXNzX3Bvc3RhbF9jb2RlIn0seyJwYXRoIjpbImFkZHJlc3MiLCJzdHJlZXRfYWRkcmVzcyJdLCJpZCI6ImFkZHJlc3Nfc3RyZWV0X2FkZHJlc3MifSx7InBhdGgiOlsibmF0aW9uYWxpdGllcyJdLCJpZCI6Im5hdGlvbmFsaXRpZXMifV19XSwic3ViIjoib3JnYW5pemF0aW9uSWRlbnRpZmllcj1ERTpUWC0xMjM0NTg3ODkgKyBvcmdhbml6YXRpb25JZGVudGlmaWVyPUVJOlNFLTU1Njc5NjE0MzMgKyBvcmdhbml6YXRpb25JZGVudGlmaWVyPUVPUklERS0xMzM0NTY3ODkwICsgQ049UmVseWluZyBQYXJ0eSBOYW1lICsgb3JnYW5pemF0aW9uSWRlbnRpZmllcj1MRUlYRy01Mjk5MDFUOEJNNDlBVVJTRE81NSIsImp0aSI6ImJkYzBlMjhmLTljZGUtNDM1Mi04MGJkLTU5OGE5YTZlYTBlMCIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZHgiOjU5NTEsInVyaSI6Imh0dHBzOi8vZnVua2Utd2FsbGV0LmRlL3N0YXR1cy1tYW5hZ2VtZW50L3N0YXR1cy1saXN0In19LCJwdWJsaWNfYm9keSI6ZmFsc2UsImVudGl0bGVtZW50cyI6W10sInNlcnZpY2VzIjpbXX0.i5gYGW-JqWvkjAnco-Q2rbZLMjjNw2lEpXz1We6mpySn6XkdjdJLLtclYh1R0utM9l3mkt6igq5B5mAYpXTDTw' diff --git a/agent/src/iaca-x509-certs.ts b/agent/src/iaca-x509-certs.ts new file mode 100644 index 0000000..631f8e5 --- /dev/null +++ b/agent/src/iaca-x509-certs.ts @@ -0,0 +1,790 @@ +export const IACA_RDW_EU_PID = `-----BEGIN CERTIFICATE----- +MIIDHTCCAqOgAwIBAgIUVqjgtJqf4hUYJkqdYzi+0xwhwFYwCgYIKoZIzj0EAwMw +XDEeMBwGA1UEAwwVUElEIElzc3VlciBDQSAtIFVUIDAxMS0wKwYDVQQKDCRFVURJ +IFdhbGxldCBSZWZlcmVuY2UgSW1wbGVtZW50YXRpb24xCzAJBgNVBAYTAlVUMB4X +DTIzMDkwMTE4MzQxN1oXDTMyMTEyNzE4MzQxNlowXDEeMBwGA1UEAwwVUElEIElz +c3VlciBDQSAtIFVUIDAxMS0wKwYDVQQKDCRFVURJIFdhbGxldCBSZWZlcmVuY2Ug +SW1wbGVtZW50YXRpb24xCzAJBgNVBAYTAlVUMHYwEAYHKoZIzj0CAQYFK4EEACID +YgAEFg5Shfsxp5R/UFIEKS3L27dwnFhnjSgUh2btKOQEnfb3doyeqMAvBtUMlClh +sF3uefKinCw08NB31rwC+dtj6X/LE3n2C9jROIUN8PrnlLS5Qs4Rs4ZU5OIgztoa +O8G9o4IBJDCCASAwEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBSzbLiR +FxzXpBpmMYdC4YvAQMyVGzAWBgNVHSUBAf8EDDAKBggrgQICAAABBzBDBgNVHR8E +PDA6MDigNqA0hjJodHRwczovL3ByZXByb2QucGtpLmV1ZGl3LmRldi9jcmwvcGlk +X0NBX1VUXzAxLmNybDAdBgNVHQ4EFgQUs2y4kRcc16QaZjGHQuGLwEDMlRswDgYD +VR0PAQH/BAQDAgEGMF0GA1UdEgRWMFSGUmh0dHBzOi8vZ2l0aHViLmNvbS9ldS1k +aWdpdGFsLWlkZW50aXR5LXdhbGxldC9hcmNoaXRlY3R1cmUtYW5kLXJlZmVyZW5j +ZS1mcmFtZXdvcmswCgYIKoZIzj0EAwMDaAAwZQIwaXUA3j++xl/tdD76tXEWCikf +M1CaRz4vzBC7NS0wCdItKiz6HZeV8EPtNCnsfKpNAjEAqrdeKDnr5Kwf8BA7tATe +hxNlOV4Hnc10XO1XULtigCwb49RpkqlS2Hul+DpqObUs +-----END CERTIFICATE-----` +export const GOOGLE_CM_WALLET = `-----BEGIN CERTIFICATE----- +MIICkjCCAjmgAwIBAgIUIrllgKEU8qxSupeyniOVstAG4SgwCgYIKoZIzj0EAwIw +eTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1v +dW50YWluIFZpZXcxHDAaBgNVBAoME0RpZ2l0YWwgQ3JlZGVudGlhbHMxHzAdBgNV +BAMMFmRpZ2l0YWxjcmVkZW50aWFscy5kZXYwHhcNMjUwMjE4MTg0MDU3WhcNMzUw +MjA2MTg0MDU3WjB5MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEW +MBQGA1UEBwwNTW91bnRhaW4gVmlldzEcMBoGA1UECgwTRGlnaXRhbCBDcmVkZW50 +aWFsczEfMB0GA1UEAwwWZGlnaXRhbGNyZWRlbnRpYWxzLmRldjBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABNcHRK+Y2b9qPzjSGABUP3IKOJu5/sYBCur6sTV7AHIb +OG/YbBPCOWwAQQNnTaBWk8tey63NgOvp8IphAjuSVlqjgZ4wgZswHQYDVR0OBBYE +FKJP9InZfEbobqOG2UdIzsy+3M/1MB8GA1UdIwQYMBaAFKJP9InZfEbobqOG2UdI +zsy+3M/1MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCoGA1Ud +EgQjMCGGH2h0dHBzOi8vZGlnaXRhbC1jcmVkZW50aWFscy5kZXYwCQYDVR0fBAIw +ADAKBggqhkjOPQQDAgNHADBEAiA1snaKSxWSuQc45aZ5mBdYI7OVB1qzAiel0vVA +B+kN6gIgKp/V7J2+2AAIRFfgexm7+72NaWGCkygXWfRPpbJ1eDk= +-----END CERTIFICATE-----` +export const IACA_Luxembourg_TST = `-----BEGIN CERTIFICATE----- +MIIC4DCCAoWgAwIBAgIUdh1IYOLqid+fbfiMWqlynOOJGOcwCgYIKoZIzj0EAwIw +cjELMAkGA1UEBhMCTFUxEzARBgNVBAgMCkx1eGVtYm91cmcxEzARBgNVBAcMCkx1 +eGVtYm91cmcxDTALBgNVBAoMBENUSUUxGDAWBgNVBAsMD1BPVEVOVElBTCBHUkVF +TjEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yNTAxMjgxMTA2NDZaFw0yNTAyMjcxMTA2 +NDZaMHIxCzAJBgNVBAYTAkxVMRMwEQYDVQQIDApMdXhlbWJvdXJnMRMwEQYDVQQH +DApMdXhlbWJvdXJnMQ0wCwYDVQQKDARDVElFMRgwFgYDVQQLDA9QT1RFTlRJQUwg +R1JFRU4xEDAOBgNVBAMMB1Jvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC +AARZyiH7PUEoRpszNFk4cxZC2Fo6a/boaKNWqVHAyDZ/irJdcgXWNqyIp3rQgvco +cEZyX8usPkEefq9Kiwq7BnKEo4H4MIH1MB0GA1UdDgQWBBRFbivCwHr8LtM8nFQm +AoI5bvkFyjCBrwYDVR0jBIGnMIGkgBRFbivCwHr8LtM8nFQmAoI5bvkFyqF2pHQw +cjELMAkGA1UEBhMCTFUxEzARBgNVBAgMCkx1eGVtYm91cmcxEzARBgNVBAcMCkx1 +eGVtYm91cmcxDTALBgNVBAoMBENUSUUxGDAWBgNVBAsMD1BPVEVOVElBTCBHUkVF +TjEQMA4GA1UEAwwHUm9vdCBDQYIUdh1IYOLqid+fbfiMWqlynOOJGOcwEgYDVR0T +AQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAgQwCgYIKoZIzj0EAwIDSQAwRgIh +AMyRr/RRJgl+UUJUz1uqx9tlu2GsVS6jRY5QaX8zTKGkAiEA/q8iR9+QAlFiB76t +qwAcVUcE5AAggHZQ7mndOG9iusA= +-----END CERTIFICATE-----` +export const IACA_Clear = `-----BEGIN CERTIFICATE----- +MIICEjCCAZmgAwIBAgIQLG8FVaMOC63vrKLhW0cgVzAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJVUzEUMBIGA1UECgwLQUxDTEVBUiBMTEMxJjAkBgNVBAMMHUNMRUFS +IElBQ0EgUm9vdCAoRGV2ZWxvcG1lbnQpMB4XDTIyMDUwMjE2MDY1N1oXDTI3MDUw +MTE3MDY1N1owSzELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FMQ0xFQVIgTExDMSYw +JAYDVQQDDB1DTEVBUiBJQUNBIFJvb3QgKERldmVsb3BtZW50KTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABLOdIj+dM2e7uaLWV5WhRan0NH6Et1ATGSO3Y7STauR2IXgt +Klm0hi0OAZcyfTgFvH+VW96ERKafHWyFKnxqIbleOg4f42IYRXclrtAViwxC56/h +rQ59sQZFU34uQ/28zqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULHPT +yedX4iQvArIc0CHAU5Q0jVowDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2cA +MGQCMGHFcy3LxOTNrBdwsjIoWnw8Gpx6uezIzclZ/tT5Qv1Q9AJfGtLEd6XukkVo +5x0/SgIwcaUppcv4nZDXdZceOQJsHCMC4POSgGjAWDU8jbVCMwyJ/R0JiKGvj6yn +d7R9rKBB +-----END CERTIFICATE----- +` +export const IACA_COI_Poland = `-----BEGIN CERTIFICATE----- +MIICfzCCAiWgAwIBAgIUR7o6iaYoONy7h7MzzTsBlUacK+UwCgYIKoZIzj0EAwIw +XDELMAkGA1UEBhMCUEwxJjAkBgNVBAoMHUNlbnRyYWxueSBPc3JvZGVrIEluZm9y +bWF0eWtpMSUwIwYDVQQDDBxjb2ktd2FsbGV0LXRlc3QtZGV2ZWxvcC1yb290MB4X +DTI1MDIyNzA4NTMxNFoXDTI2MDIyNzA4NTMxNFowXDELMAkGA1UEBhMCUEwxJjAk +BgNVBAoMHUNlbnRyYWxueSBPc3JvZGVrIEluZm9ybWF0eWtpMSUwIwYDVQQDDBxj +b2ktd2FsbGV0LXRlc3QtZGV2ZWxvcC1yb290MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEOV4NyiWOCbgerpCWjpQkyigmZCfLGfUSKBYVTtsWSwI2ci5Khz27mNGu +BWfd5ArDUU2cZL0f7Iig8XELxykA6qOBxDCBwTA7BgNVHRIENDAyhjBodHRwczov +L2FwaS5kZXZlbG9wLmFwaS5tb2J5d2F0ZWwucHJvLmNvaS5nb3YucGwwDwYDVR0T +BAgwBgEB/wIBADBFBgNVHR8EPjA8MDqgOKA2hjRodHRwczovL2FwaS5kZXZlbG9w +LmFwaS5tb2J5d2F0ZWwucHJvLmNvaS5nb3YucGwvY3JsMB0GA1UdDgQWBBSl8Ga5 +kk3TQ9YvENQCJDiC2HCKSTALBgNVHQ8EBAMCAQYwCgYIKoZIzj0EAwIDSAAwRQIh +APCGTQjKBfXHuLWg5Q6xOTKWfhBpi8lg57+wAiUFdSOjAiArFQAhQ2dKgqiy5bCS +V8rw9+qYyVnSWGTRduy8NX70Zw== +-----END CERTIFICATE----- + +` +export const IACA_Google = `-----BEGIN CERTIFICATE----- +MIIChzCCAg2gAwIBAgIBATAKBggqhkjOPQQDAzA5MSowKAYDVQQDDCFPV0YgSWRlbnRpdHkgQ3Jl +ZGVudGlhbCBURVNUIElBQ0ExCzAJBgNVBAYMAlpaMB4XDTI0MTIwMTAwMDAwMFoXDTM0MTIwMTAw +MDAwMFowOTEqMCgGA1UEAwwhT1dGIElkZW50aXR5IENyZWRlbnRpYWwgVEVTVCBJQUNBMQswCQYD +VQQGDAJaWjB2MBAGByqGSM49AgEGBSuBBAAiA2IABPkA8nu9JtjtJZT1zI1Y8VWc95uZOmoE/sIo +fi+/W+48qlJffbG3lJ6cWiw/nJgdxyt7cJAO35lSUqGwXPvQg4ZId5sep/mKB+UbpWklk4VgXzMk +Y7H1Tg5KLBywg52z1aOB6DCB5TAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADBM +BgNVHRIERTBDhkFodHRwczovL2dpdGh1Yi5jb20vb3BlbndhbGxldC1mb3VuZGF0aW9uLWxhYnMv +aWRlbnRpdHktY3JlZGVudGlhbDBSBgNVHR8ESzBJMEegRaBDhkFodHRwczovL2dpdGh1Yi5jb20v +b3BlbndhbGxldC1mb3VuZGF0aW9uLWxhYnMvaWRlbnRpdHktY3JlZGVudGlhbDAdBgNVHQ4EFgQU +q2Ub4FbCkFPx3X9s5Ie+aN5gyfUwCgYIKoZIzj0EAwMDaAAwZQIxAJJDZ5Js5BdvcpxcaTW8nqHD +MsvyEPFSaM2AvfF4YHUmlNiAibbJpbw5QoVoaAV5ugIwbycvXRI6z6m2pxlC0y/Dp0MI+87JHFMK ++WcuHCgIGPfHBqR4T1XMMqyFeFrO4AKA +-----END CERTIFICATE-----` +export const IACA_LT = `-----BEGIN CERTIFICATE----- +MIID9TCCA5qgAwIBAgIUfUTnHNr3mQGnBjNNHiZCIPrVRuowCgYIKoZIzj0EAwIw +gZ8xCzAJBgNVBAYTAkxUMR0wGwYDVQQKDBRNaW5pc3RyeSBvZiBJbnRlcmlvcjEq +MCgGA1UECwwhRVVESVcgUG90ZW50aWFsIFByb2plY3QgKFJvb3QgQ0EpMRIwEAYD +VQQIDAlMaXRodWFuaWExEDAOBgNVBAcMB1ZpbG5pdXMxHzAdBgNVBAMMFmNhLmV1 +ZGl3LWx0Lmxlbmdvci5kZXYwHhcNMjUwMjI2MjIyMzExWhcNMjYwMjI2MjIyMzEx +WjCBnzELMAkGA1UEBhMCTFQxHTAbBgNVBAoMFE1pbmlzdHJ5IG9mIEludGVyaW9y +MSowKAYDVQQLDCFFVURJVyBQb3RlbnRpYWwgUHJvamVjdCAoUm9vdCBDQSkxEjAQ +BgNVBAgMCUxpdGh1YW5pYTEQMA4GA1UEBwwHVmlsbml1czEfMB0GA1UEAwwWY2Eu +ZXVkaXctbHQubGVuZ29yLmRldjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBgA +dDPcDkSD4PrZCEPg5fTMHR+s+FJ0oX6YWG+9ryjOvhoDpK6u7GCf6+a951CxDfrb +5MyIj5DR+Q+xiGT7jQmjggGwMIIBrDAdBgNVHQ4EFgQUCEyob4WB0iaZahw/RWlH +WSF0q2Ywgd8GA1UdIwSB1zCB1IAUCEyob4WB0iaZahw/RWlHWSF0q2ahgaWkgaIw +gZ8xCzAJBgNVBAYTAkxUMR0wGwYDVQQKDBRNaW5pc3RyeSBvZiBJbnRlcmlvcjEq +MCgGA1UECwwhRVVESVcgUG90ZW50aWFsIFByb2plY3QgKFJvb3QgQ0EpMRIwEAYD +VQQIDAlMaXRodWFuaWExEDAOBgNVBAcMB1ZpbG5pdXMxHzAdBgNVBAMMFmNhLmV1 +ZGl3LWx0Lmxlbmdvci5kZXaCFH1E5xza95kBpwYzTR4mQiD61UbqMBIGA1UdEwEB +/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMFYGA1UdHwRPME0wS6BJoEeGRWh0 +dHBzOi8vc3RvcmFnZS5nb29nbGVhcGlzLmNvbS9ldWRpdy1sdC13YWxsZXQtZG93 +bmxvYWRzL2NybC5jZXJ0LnBlbTAtBgNVHRIEJjAkhiJodHRwczovL3dhbGxldC5l +dWRpdy1sdC5sZW5nb3IuZGV2MAoGCCqGSM49BAMCA0kAMEYCIQDx4xniqGJd92kS +g3j5AVSapgWP3FeoMoB0KLx4DnuMcQIhAJO6hO4T8JQ4hgjIQbls0GaVyX0Y3Fzq +ujKF5by16M2C +-----END CERTIFICATE----- +` +export const IACA_Google_mDL = `-----BEGIN CERTIFICATE----- +MIIDGDCCAnqgAwIBAgIUbmNY6ouM3zk/ByVfQQr9NSR3hY0wCgYIKoZIzj0EAwQw +QDEPMA0GA1UECgwGR29vZ2xlMQswCQYDVQQGEwJaWjEgMB4GA1UEAwwXVUwgVEVT +VElORyBTdGFnaW5nIFJvb3QwHhcNMjQwNDI2MTU0OTEwWhcNMjUwNDI2MTU0OTEw +WjBAMQ8wDQYDVQQKDAZHb29nbGUxCzAJBgNVBAYTAlpaMSAwHgYDVQQDDBdVTCBU +RVNUSU5HIFN0YWdpbmcgUm9vdDCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEASMK +6z32jHX91q2BE5GoO8mrfj5ebWR552INRdpwVb5HMwU67iwrXIMOWKpa1dOEeLyN +NRWyhiiHPEKY9s66YVO8ACsKk0XkGhgVG0NNwwQpnRlnk0zw4sM74UytZmroYmgN +sD4eQOSB0hff9euHnYJlFzW6U6VaPPC27V5IUt9d9WVjo4IBDTCCAQkwHQYDVR0O +BBYEFHOnBB5QgZOe4Nu3AorjcS/QqjbBMB8GA1UdIwQYMBaAFHOnBB5QgZOe4Nu3 +AorjcS/QqjbBMA8GA1UdEwQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCEGA1Ud +EgQaMBiGFmh0dHBzOi8vd3d3Lmdvb2dsZS5jb20wgYIGA1UdHwR7MHkwd6B1oHOG +cWh0dHA6Ly9wcml2YXRlY2EtY29udGVudC02NjAzYzY0Ny0wMDAwLTI5ZDQtYWVh +Ni1mNGY1ZTgwZDFiOTAuc3RvcmFnZS5nb29nbGVhcGlzLmNvbS80MWQ0MzY4NTll +NDVkYmYzZTVhOS9jcmwuY3JsMAoGCCqGSM49BAMEA4GLADCBhwJBL2JQmB8ktt3A +evEdJbOcWq5S76AHoGmrsoV7YYIGIUGBPdOSWkFCvb+bXMLSGK4VDCz1/qL1KvRE ++v9MK4ykXPsCQgFM1IJY2hQKgaWW2cmcwYktvhlFNJguZK2v7zmLXjYHbwZPj2uR +kbgrm3zKoyV95hqE6sblsc8E1ZDzGGdzQ5X7sw== +-----END CERTIFICATE-----` +export const IACA_Luxembourg_QUA = `-----BEGIN CERTIFICATE----- +MIIC4TCCAoigAwIBAgIUcNR+sK6E7GztaDc8jqgJT9Ae1fAwCgYIKoZIzj0EAwIw +czELMAkGA1UEBhMCTFUxEzARBgNVBAgMCkx1eGVtYm91cmcxEzARBgNVBAcMCkx1 +eGVtYm91cmcxDTALBgNVBAoMBENUSUUxGTAXBgNVBAsMEFBPVEVOVElBTCBPUkFO +R0UxEDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjUwMTMwMTAxMTE0WhcNMjUwMzAxMTAx +MTE0WjBzMQswCQYDVQQGEwJMVTETMBEGA1UECAwKTHV4ZW1ib3VyZzETMBEGA1UE +BwwKTHV4ZW1ib3VyZzENMAsGA1UECgwEQ1RJRTEZMBcGA1UECwwQUE9URU5USUFM +IE9SQU5HRTEQMA4GA1UEAwwHUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABD83yvp6C5YbZrhxDGqUFGvCF2vbFkzEt76oWgZ2kMOVppJhPb8HcbV9rlyQ +C9I4P2Iskm4x0LrNDUy+x4WDXNCjgfkwgfYwHQYDVR0OBBYEFI239BL16o4Lrivo +POFyWPJBtXM6MIGwBgNVHSMEgagwgaWAFI239BL16o4LrivoPOFyWPJBtXM6oXek +dTBzMQswCQYDVQQGEwJMVTETMBEGA1UECAwKTHV4ZW1ib3VyZzETMBEGA1UEBwwK +THV4ZW1ib3VyZzENMAsGA1UECgwEQ1RJRTEZMBcGA1UECwwQUE9URU5USUFMIE9S +QU5HRTEQMA4GA1UEAwwHUm9vdCBDQYIUcNR+sK6E7GztaDc8jqgJT9Ae1fAwEgYD +VR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAgQwCgYIKoZIzj0EAwIDRwAw +RAIgGHwSoajAJ71lFMkQC4gkRO0KhDjF4j31NlY4TIYoXdYCIDQq5obM1/BY+62X +7m/UBPVhgi19Pv892D7bBwrQxosU +-----END CERTIFICATE-----` +export const SICPA_sub_IACA = `-----BEGIN CERTIFICATE----- +MIIGKzCCBBOgAwIBAgIIWVMmyJNnl9IwDQYJKoZIhvcNAQELBQAwTjEoMCYGA1UEAwwfSW50ZXNpIEdyb3VwIENsb3VkIFJvb3QgQ0EgVGVzdDEVMBMGA1UECgwMSW50ZXNpIEdyb3VwMQswCQYDVQQGEwJJVDAeFw0xNzA3MTkxNDE0NDFaFw0yNzA3MTkxNDE0NDFaMIGzMTowOAYDVQQDDDFJbnRlc2kgR3JvdXAgRVUgUXVhbGlmaWVkIEVsZWN0cm9uaWMgU2VhbCBDQSBUZXN0MS4wLAYDVQQLDCVUZXN0IFF1YWxpZmllZCBUcnVzdCBTZXJ2aWNlIFByb3ZpZGVyMRwwGgYDVQQKDBNJbnRlc2kgR3JvdXAgUy5wLkEuMRowGAYDVQRhDBFWQVRJVC0wMjc4MDQ4MDk2NDELMAkGA1UEBhMCSVQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC52YfpEAw+S3qkLqKMz39hK5ipSbUiET79IYRz6ye+VDUPnertKbfE1NyaCl3U7VS3lDGvGgVqbP0R4Zy7KZ8/Muc5nfWvPQOC0GkkZ9uJqxFkeC/ktAIxwG3KpO90jU2GJhoKs1JiUfCJ9GMeFc1Y4jpI/I8XwpsoX5vmU61oEWwwOdWynoSJ9Qv5R2BoXqDNLiB24rNy6RjmZXOIcnrQegDIzx7s0AoedkqxZdRE409pweDlJjYNb+ZYpmsqxI+FjqypnV5gDS8RmDVb8nUvLMmH6yQAnxz5AM4irHAv2/7wgZVnr/FWriMkJoZCqQLbY4CoyR9Tz2Tdl1LE+lgk/ZVTpyS+EVQZWv8ZA7UiUzwwzaQLoIHmPlVsO2qg2aya5YngX1RbQk/EN/QKVdEtWlDbB9mON86dCW+sOAg+HPxlkzAGo22rwy37L4n13SuzYtBqIc0E0ndLhZY3tSR2vjE1NKc/ymiV08jyTnDcbuKyKfwEY44K6PZYcWynRjLHoEDpCZUiiahw9h/Z55pFZNx7A2tgIKWCsB8Ec3X+Q6T24jDsUvde+rbdG7u0wO6gl+aJeb/A5DUe0/8pAQcYhAXgqEbaCy9wbH3lilnT/W1ruWD2Zt7yxC7wCsoJHGMaurK5f5cPENSgVrza6sLZQmGdRo2MGi22WR2bJqV+zwIDAQABo4GmMIGjMB0GA1UdDgQWBBTIAvaUVncjlpJyS+0tGxs4+W3eCDAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFOmPxfZlDmkDro8vSPiEFshl2dHrMEAGA1UdHwQ5MDcwNaAzoDGGL2h0dHA6Ly9jcmwudGVzdDRtaW5kLmNvbS9JbnRlc2kvQ2xvdWRSb290Q0EuY3JsMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAShWSXbY2dzEYQ9XzAfAEqQ4InKlYPBwuUgNfnQPJ+SoI2vJ82SkybH+1CLY8Voup3gRUr2t5QkBs1NVazSEXdj6/uO6Mhk+OnvWvz+8ObMTshM1EGcrLH046BO9eh30efX7LLbbBBxq3lEnt6PmF4PLY1eNXytRD6XK5Va9IfgiVBLdUEk+ITITsHQCWzZ9F5J6Z3aPbHLoXKk8XKfLmVzgLU9Yt2Nc5DKGHOS3ALYen9HrQWrzM7n1OcwyxLz7VMrH3pxiCaltz+HgZwttX8P80b+oVr00OKfiGxEEj+VcCHhnKwjvQFM341UhLh+WPhrIYr/dQeZp9Mk91soDX/W6wVuEBCYkujprN/sU/kI1YYmOrq1mXwPo7YN0Kpo1CgfWFkw9qvJsHm/hZIdepIZ3xljGf1/esaEM1F6uuZW+SJifq+O6yxnfm3wBa0mOdxLA9qbnlEKs9kd0afAio00WoT2I+MIsKwV1ZPwdHDApfuY2NO10SVEl3R5x4JUyX3+0YMoA+u6DipubWcFZg3BgVVBeCjsWO5ftYAHPqaufOfsibsP0BbRIak2CRZ3mFXYScQ9I2eiTiGC4iZdc0wifIZGgrHQUQN5+mYIK9WN3k9WEP4blnVurguKQ9oHxONHFIckAYM5nbnC/mA/N9ULs9NSP+csp/5Kwy8/DtYhc= +-----END CERTIFICATE-----` +export const IACA_COI_Poland_mDL = `-----BEGIN CERTIFICATE----- +MIIDRDCCAuugAwIBAgIUcINSwrr4ij003of3lyYBlUakOP8wCgYIKoZIzj0EAwIw +XDELMAkGA1UEBhMCUEwxJjAkBgNVBAoMHUNlbnRyYWxueSBPc3JvZGVrIEluZm9y +bWF0eWtpMSUwIwYDVQQDDBxjb2ktd2FsbGV0LXRlc3QtZGV2ZWxvcC1yb290MB4X +DTI1MDIyNzA5MDIwMloXDTI2MDIyNzA5MDIwMlowYjELMAkGA1UEBhMCUEwxJjAk +BgNVBAoMHUNlbnRyYWxueSBPc3JvZGVrIEluZm9ybWF0eWtpMSswKQYDVQQDDCJw +dWItZWFhLXByb3ZpZGVyLXRlc3QtZGV2ZWxvcC1sZWFmMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEGhoJtJntrCr8nJp/Kp9BDWeZZ2IVnuHGvm6BcPV/I9T9gBst +0piyfaNQ3miYEwLjQAYSKa54JhDcyKrvLOB7y6OCAYMwggF/MFYGA1UdHwRPME0w +S6BJoEeGRWh0dHBzOi8vYXBpLmRldmVsb3AuYXBpLm1vYnl3YXRlbC5wcm8uY29p +Lmdvdi5wbC9wdWItZWFhLXByb3ZpZGVyL2NybDBMBgNVHRIERTBDhkFodHRwczov +L2FwaS5kZXZlbG9wLmFwaS5tb2J5d2F0ZWwucHJvLmNvaS5nb3YucGwvcHViLWVh +YS1wcm92aWRlcjAPBgNVHRMECDAGAQH/AgEAMIGZBgNVHSMEgZEwgY6AFKXwZrmS +TdND1i8Q1AIkOILYcIpJoWCkXjBcMQswCQYDVQQGEwJQTDEmMCQGA1UECgwdQ2Vu +dHJhbG55IE9zcm9kZWsgSW5mb3JtYXR5a2kxJTAjBgNVBAMMHGNvaS13YWxsZXQt +dGVzdC1kZXZlbG9wLXJvb3SCFEe6OommKDjcu4ezM807AZVGnCvlMB0GA1UdDgQW +BBQ3Mj4TetCUgMCOqBnBwzost1A0TjALBgNVHQ8EBAMCB4AwCgYIKoZIzj0EAwID +RwAwRAIgUYJ5ofx5dFSzSTI1T0zRtKb29ghtw7TTs7JztanB+wsCIAZ+iy4s0lF5 +XLlK4gvYy9/7YcDrtUvHgG5+nyQfQSXN +-----END CERTIFICATE----- + +` +export const IACA_BDR = `-----BEGIN CERTIFICATE----- +MIICQjCCAeigAwIBAgIUdYKlPOH6l5pLDwFIXiHZ7vZUv+EwCgYIKoZIzj0EAwIw +RDELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEkMCIGA1UEAwwbQkRSIEkg +VGVzdCBDZXJ0aWZpY2F0ZSBJQUNBMB4XDTI1MDIxOTEyNTQ1OFoXDTM1MDIxNzEy +NTQ1OFowRDELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEkMCIGA1UEAwwb +QkRSIEkgVGVzdCBDZXJ0aWZpY2F0ZSBJQUNBMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEVFHEmjNdN3hZIq2UHUG4iyyPGbYRD0puDP1yjXaRYm5l1jp0qbYynPQq +HzNMq0xkXDtkA1YWvQAEkjwooAgok6OBtzCBtDAdBgNVHQ4EFgQULREevg/dgMFy +m9MGqJWSPPAV0aswHwYDVR0jBBgwFoAULREevg/dgMFym9MGqJWSPPAV0aswEgYD +VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0SBBYwFIESZXhh +bXBsZUBpc29tZGwuY29tMC8GA1UdHwQoMCYwJKAioCCGHmh0dHBzOi8vZXhhbXBs +ZS5jb20vSVNPbURMLmNybDAKBggqhkjOPQQDAgNIADBFAiAFBk1r4NVyRbxxiWz0 +f37c4ZTWjtpO3KVt3wVNm39aVgIhAP0eZlndKiiiMA/ALCkOffv2oKbYpz+5+gI2 +i3Xf6pPx +-----END CERTIFICATE----- +` +export const IACA_Panasonic = `-----BEGIN CERTIFICATE----- +MIICOTCCAb+gAwIBAgIUEmXprSfio3PYApPTduV3ByNcZ6QwCgYIKoZIzj0EAwMw +JjEXMBUGA1UEAwwOUGFuYXNvbmljIElBQ0ExCzAJBgNVBAYTAkpQMB4XDTI1MDIx +NTAyMDk0MloXDTMwMDYxODAyMDk0MVowJjEXMBUGA1UEAwwOUGFuYXNvbmljIElB +Q0ExCzAJBgNVBAYTAkpQMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOiJgneJq6Kdx +B7JA8WjkyA+TmZFL9yr9xGwsn1a4bnSexgNBwfidqjW476GUlm4ihffdlsy8Jkrw +dlNHxKJyL0z0ItmjfhU++ELHLJVhmgXniI7TxtXYeATY10oywCogo4GtMIGqMBIG +A1UdEwEB/wQIMAYBAf8CAQAwKgYDVR0SBCMwIYYfaHR0cHM6Ly9tZG9jLnBhbmFz +b25pYy5nb3YvbWRvYzA5BgNVHR8EMjAwMC6gLKAqhihodHRwczovL21kb2MucGFu +YXNvbmljLmdvdi9DUkxzL21kb2MuY3JsMB0GA1UdDgQWBBSF8+ke5XiOT6SjyIbp +TTdRKQYvMDAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwDGczzKzj +dITy5y/y7C3UoFIhfLMFYIP/Ijr7OmA9NbEqivXbcbh67gCLacZPxfNvAjEAidrs +839DN0Jo/pUCf+FvCIqgU7ndo2K+rlBAfNbOcV11Ckg1RoSb/Pftfw04nAci +-----END CERTIFICATE----- +` +export const IACA_Bosa = `-----BEGIN CERTIFICATE----- +MIICuTCCAl6gAwIBAgIUZskLLyPI1UImvhdUb784UWnrNK0wCgYIKoZIzj0EAwIw +gY0xCzAJBgNVBAYTAkJFMRgwFgYDVQQIDA9CcnVzc2VscyBSZWdpb24xDTALBgNV +BAoMBEJPU0ExETAPBgNVBAsMCEJPU0EtSUFBMRUwEwYDVQQDDAxCT1NBIENBIFRl +c3QxKzApBgkqhkiG9w0BCQEWHG9saXZpZXIucGllcnJldEBib3NhLmZnb3YuYmUw +HhcNMjUwMjIwMTYyMTQ4WhcNMzAwMjIwMTYyMTQ4WjCBjTELMAkGA1UEBhMCQkUx +GDAWBgNVBAgMD0JydXNzZWxzIFJlZ2lvbjENMAsGA1UECgwEQk9TQTERMA8GA1UE +CwwIQk9TQS1JQUExFTATBgNVBAMMDEJPU0EgQ0EgVGVzdDErMCkGCSqGSIb3DQEJ +ARYcb2xpdmllci5waWVycmV0QGJvc2EuZmdvdi5iZTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABPE08jfo13LHuy92aHtL5M4au7Eftr/LgAqjgZx/P1E7a4+V+9dK +gbEB3syXNmL31OYBXw8WyTRc9GJ/OkLndp2jgZkwgZYwEgYDVR0TAQH/BAgwBgEB +/wIBADAOBgNVHQ8BAf8EBAMCAQYwJwYDVR0SBCAwHoEcb2xpdmllci5waWVycmV0 +QGJvc2EuZmdvdi5iZTAoBgNVHR8EITAfMB2gG6AZhhdodHRwczovL2JkaXcuYmUv +bHNwL2NybDAdBgNVHQ4EFgQUyL/DuDsv/CBcoA1hqnS7c38sSlQwCgYIKoZIzj0E +AwIDSQAwRgIhANxqTppStE2/m4GMv+OL0wRKOvgED/NtRLBvnUz7n4ybAiEAvsZh +qT82adcttIV80a2tjK4on9GJHjqqg4d7q5EGKu0= +-----END CERTIFICATE----- +` +export const Intermediate_IACA_Luxembourg_QUA = `-----BEGIN CERTIFICATE----- +MIIC3zCCAoWgAwIBAgIURcEVDGuoXwERy7PVmHmQhOVlERUwCgYIKoZIzj0EAwIw +czELMAkGA1UEBhMCTFUxEzARBgNVBAgMCkx1eGVtYm91cmcxEzARBgNVBAcMCkx1 +eGVtYm91cmcxDTALBgNVBAoMBENUSUUxGTAXBgNVBAsMEFBPVEVOVElBTCBPUkFO +R0UxEDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjUwMTMwMTAxMTE1WhcNMjUwMzAxMTAx +MTE1WjBwMQswCQYDVQQGEwJMVTETMBEGA1UECAwKTHV4ZW1ib3VyZzETMBEGA1UE +BwwKTHV4ZW1ib3VyZzENMAsGA1UECgwEQ1RJRTEZMBcGA1UECwwQUE9URU5USUFM +IE9SQU5HRTENMAsGA1UEAwwEQ0EgMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA +BOvvCWDTPmkkaJJgcPdpq8D4X85d9OkET/3ueAgvYIlbUZVAb7NMjeNheEPxIUVa +5SXejSl5x9m6yeomKOB9gQWjgfkwgfYwHQYDVR0OBBYEFJkOqxzpxP/VoF9FaSbh +2gT0huCkMIGwBgNVHSMEgagwgaWAFI239BL16o4LrivoPOFyWPJBtXM6oXekdTBz +MQswCQYDVQQGEwJMVTETMBEGA1UECAwKTHV4ZW1ib3VyZzETMBEGA1UEBwwKTHV4 +ZW1ib3VyZzENMAsGA1UECgwEQ1RJRTEZMBcGA1UECwwQUE9URU5USUFMIE9SQU5H +RTEQMA4GA1UEAwwHUm9vdCBDQYIUcNR+sK6E7GztaDc8jqgJT9Ae1fAwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAgQwCgYIKoZIzj0EAwIDSAAwRQIh +APELtvzyANvm+YfJnNSXUTwwKgaFIGCxDAAZ3aPBolP2AiBft4ejSc/sk30iEpYM +N+HyqgYW2jeVj1c3Sqi2koyclA== +-----END CERTIFICATE----- +` +export const IACA_Toppan_mDL = `-----BEGIN CERTIFICATE----- +MIICgDCCAiagAwIBAgIJAdGsox/oZjHWMAoGCCqGSM49BAMCME8xCzAJBgNVBAYT +AlVTMQwwCgYDVQQKDANISUQxDDAKBgNVBAsMA0NJRDEUMBIGA1UEAwwLZ29JRG1E +TElBQ0ExDjAMBgNVBAgMBVVTLVRYMB4XDTI0MDczMTAwMDAwMFoXDTMzMDczMTIz +NTk1OVowTzELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA0hJRDEMMAoGA1UECwwDQ0lE +MRQwEgYDVQQDDAtnb0lEbURMSUFDQTEOMAwGA1UECAwFVVMtVFgwWTATBgcqhkjO +PQIBBggqhkjOPQMBBwNCAARUz2Hp/0f2P1IPWpP+2Ab4DBrdJVEXBeLi4P6ulmJ8 +dK2QfzRu3x1QaLTMYRjl61r4ljtnFgO2PgTsDlR8u8vFo4HqMIHnMBIGA1UdEwEB +/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRG1Oiyl8yrfi/1 +pOcjSfcsfU1pFDArBgNVHRAEJDAigA8yMDI0MDczMTAwMDAwMFqBDzIwMjkwNzMx +MjM1OTU5WjAkBgNVHRIEHTAbhhlodHRwOi8vd3d3LmhpZGdsb2JhbC5jb20vME8G +A1UdHwRIMEYwRKBCoECGPmh0dHBzOi8vZGV2LWxzLmdvaWRodWIuaGlkY2xvdWQu +Y29tL2NpZC9oaWR0ZXN0aWFjYW1kbC5jcmwucGVtMAoGCCqGSM49BAMCA0gAMEUC +IQDVnXuCCD76kdtfWbFck0Jxsv+at/99eDzegB6gWelLvQIgLFMtV5xyB1E9mM/n +WTKzAlzrl2IBUPq4GqUW/E5GCAw= +-----END CERTIFICATE----- +` +export const IACA_IPZS = `-----BEGIN CERTIFICATE----- +MIICMjCCAdigAwIBAgIJAM/XjjPEniA7MAoGCCqGSM49BAMCMGwxCzAJBgNVBAYT +AklUMQ4wDAYDVQQIDAVMYXppbzENMAsGA1UEBwwEUm9tZTEPMA0GA1UECgwGTW9i +aWxlMQ0wCwYDVQQLDARJUFpTMR4wHAYDVQQDDBVJUFpTIElBQ0EgU2VsZi1TaWdu +ZWQwHhcNMjUwMjE0MTcyNTAwWhcNMzUwMjEyMTcyNTAwWjBsMQswCQYDVQQGEwJJ +VDEOMAwGA1UECAwFTGF6aW8xDTALBgNVBAcMBFJvbWUxDzANBgNVBAoMBk1vYmls +ZTENMAsGA1UECwwESVBaUzEeMBwGA1UEAwwVSVBaUyBJQUNBIFNlbGYtU2lnbmVk +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqR4IfnRYdEwQo12oPk0EGtJQrUaW +lb/XVUe8MIt8Q/TXChJoBeH+1tHbKIwLNYftjjXQCwPekwbxz2dRGKSIYqNjMGEw +HQYDVR0OBBYEFIed8rLRY//FFhH7ka8/oJI4/mbnMB8GA1UdIwQYMBaAFIed8rLR +Y//FFhH7ka8/oJI4/mbnMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG +MAoGCCqGSM49BAMCA0gAMEUCIA25MRulze6PjZpwGbz95BnwZ6YsijnBcwBzEudl +rNi4AiEAqbUB9kZC9kXKoU1PipkX5/aRuSpUPkSLYVuy5MYWF1c= +-----END CERTIFICATE----- +` +export const IACA_Procivis = `-----BEGIN CERTIFICATE----- +MIICLDCCAdKgAwIBAgIUQM0iVH84NMUmxcIuGibH4gMyRmgwCgYIKoZIzj0EAwQw +YjELMAkGA1UEBhMCQ0gxDzANBgNVBAcMBlp1cmljaDERMA8GA1UECgwIUHJvY2l2 +aXMxETAPBgNVBAsMCFByb2NpdmlzMRwwGgYDVQQDDBNjYS5kZXYubWRsLXBsdXMu +Y29tMB4XDTIyMDExMjEyMDAwMFoXDTMyMDExMDEyMDAwMFowYjELMAkGA1UEBhMC +Q0gxDzANBgNVBAcMBlp1cmljaDERMA8GA1UECgwIUHJvY2l2aXMxETAPBgNVBAsM +CFByb2NpdmlzMRwwGgYDVQQDDBNjYS5kZXYubWRsLXBsdXMuY29tMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEaRFtZbpYHFlPgGyZCt6bGKS0hEekPVxiBHRXImo8 +/NUR+czg+DI2KTE3ikRVNgq2rICatkvkV2jaM2frPEOl1qNmMGQwEgYDVR0TAQH/ +BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFO0asJ3iYEVQADva +WjQyGpi+LbfFMB8GA1UdIwQYMBaAFO0asJ3iYEVQADvaWjQyGpi+LbfFMAoGCCqG +SM49BAMEA0gAMEUCIQD9kfI800DOj76YsiW4lUNRZowH07j152M3UKHKEaIjUAIg +ZNINukb4SFKEC4A0qEKgpPEZM7/Vh5aNro+PQn3/rgA= +-----END CERTIFICATE-----` +export const IACA_Veridos = `-----BEGIN CERTIFICATE----- +MIICQDCCAeegAwIBAgIQTUlTVF9UZXN0X1JEUkOgwjAKBggqhkjOPQQDAjA2MQsw +CQYDVQQGEwJVUzEQMA4GA1UECgwHVmVyaWRvczEVMBMGA1UEAwwMVmVyaWRvcyBJ +QUNBMB4XDTI1MDIxMDExMDgzMVoXDTQ1MDIwNTExMDgzMVowNjELMAkGA1UEBhMC +VVMxEDAOBgNVBAoMB1Zlcmlkb3MxFTATBgNVBAMMDFZlcmlkb3MgSUFDQTBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABGvQArUnJGGUNn+XAeeu89thNQGKKULbF3sM +EnuiIX96RcrwZjGh0xJxcgD+KEscL3ZrIUH1cnRgZs9XOpEGRnejgdYwgdMwHQYD +VR0OBBYEFAy6T2h6nvdTpujuj8rm3Qqd/oxHMB8GA1UdIwQYMBaAFAy6T2h6nvdT +pujuj8rm3Qqd/oxHMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEG +MEkGA1UdHwRCMEAwPqA8oDqGOGh0dHBzOi8vaW50ZXJvcC5pbXByb3ZlaWQubmV0 +L2Rvd25sb2Fkcy92ZF9yb290X2lhY2EuY3JsMCIGA1UdEgQbMBmGF2h0dHBzOi8v +d3d3LnZlcmlkb3MuY29tMAoGCCqGSM49BAMCA0cAMEQCIEQ/XSAYBF67lX1IyJie +CnPKonui894lWRvyaMOLILglAiBBcS/2woyFzcm1L6zhbK+Gb0XGkwRhaUM9URXi +xsw7qQ== +-----END CERTIFICATE----- +` +export const IACA_OGCIO = `-----BEGIN CERTIFICATE----- +MIIC5zCCAo6gAwIBAgIQTAoIGUdncxSpGGBVlzFCajAKBggqhkjOPQQDAjA/MTAw +LgYDVQQDEydPR0NJTyBEaWdpdGFsIENyZWRlbnRpYWwgVmVyaWZpZXIgQWdlbnQx +CzAJBgNVBAYTAklFMB4XDTI1MDIyMDAwMDAwMFoXDTI2MDIyMDAwMDAwMFowPzEw +MC4GA1UEAxMnT0dDSU8gRGlnaXRhbCBDcmVkZW50aWFsIFZlcmlmaWVyIEFnZW50 +MQswCQYDVQQGEwJJRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLD+xNBjhM40 +w4A7Z9r0HH6dkjDl1whNRyHrmNjrLBtM+F08oqF/8mWmnCCG0dmGzZbmy3ijpSMC +io8pS9YFDqujggFqMIIBZjBKBgNVHQ4EQwRBBLD+xNBjhM40w4A7Z9r0HH6dkjDl +1whNRyHrmNjrLBtM+F08oqF/8mWmnCCG0dmGzZbmy3ijpSMCio8pS9YFDqswDgYD +VR0PAQH/BAQDAgGGMBUGA1UdJQEB/wQLMAkGByiBjF0FAQIwTAYDVR0jBEUwQ4BB +BLD+xNBjhM40w4A7Z9r0HH6dkjDl1whNRyHrmNjrLBtM+F08oqF/8mWmnCCG0dmG +zZbmy3ijpSMCio8pS9YFDqswFwYDVR0SBBAwDoYMb2djaW8uZ292LmllME8GA1Ud +EQRIMEaGIWFnZW50cy5wcm9kLmRpZ2l0YWwtd2FsbGV0Lmdvdi5pZYIhYWdlbnRz +LnByb2QuZGlnaXRhbC13YWxsZXQuZ292LmllMBIGA1UdEwEB/wQIMAYBAf8CAQAw +JQYDVR0fBB4wHDAaoBigFoYUaHR0cHM6Ly9vZ2Npby5nb3YuaWUwCgYIKoZIzj0E +AwIDRwAwRAIgI7laIwRyAc7xrV2ejalkjgZlmDF4MLFjYxrnhlrZblkCIA6GXQqc +Qv0WYVjtfHFpjYaS/Sw8FjoMsDx/mWywpjeP +-----END CERTIFICATE----- +` +export const IACA_Credence_ID = `-----BEGIN CERTIFICATE----- +MIIDMTCCAregAwIBAgIUDSdHgrVcoCbZWveI77AOZN7l+QUwCgYIKoZIzj0EAwMw +gakxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRAwDgYDVQQHDAdP +YWtsYW5kMRQwEgYDVQQKDAtDcmVkZW5jZSBJRDEMMAoGA1UECwwDbUlEMR4wHAYD +VQQDDBV2ZXJpZnkuY3JlZGVuY2VpZC5jb20xLzAtBgkqhkiG9w0BCQEWIHZpamV0 +aC5hcmFsYWd1cHBpQGNyZWRlbmNlaWQuY29tMB4XDTIyMDUxOTEzMTI0OFoXDTMw +MDgwNTEzMTI0OFowgakxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh +MRAwDgYDVQQHDAdPYWtsYW5kMRQwEgYDVQQKDAtDcmVkZW5jZSBJRDEMMAoGA1UE +CwwDbUlEMR4wHAYDVQQDDBV2ZXJpZnkuY3JlZGVuY2VpZC5jb20xLzAtBgkqhkiG +9w0BCQEWIHZpamV0aC5hcmFsYWd1cHBpQGNyZWRlbmNlaWQuY29tMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEllP/oweMpwuIoqx20fPkh+4RS1T6PvJnhxYUj4lxeM3q +1HnUkn2VwX8E2+UR7GROfpX0PUeeM2eNNmi3tQQY47ruB6Rb0VSxFuc4s/liZS/2 +t8nD7kSyEsRC/S8Xi72Ao4GdMIGaMB8GA1UdIwQYMBaAFCHHz5AGoVeOLgvdnBYj +FxThOTh7MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMBUGA1Ud +JQEB/wQLMAkGByiBjF0FAQcwHQYDVR0OBBYEFCHHz5AGoVeOLgvdnBYjFxThOTh7 +MB0GA1UdEgQWMBSCEnd3dy5jcmVkZW5jZWlkLmNvbTAKBggqhkjOPQQDAwNoADBl +AjAAznx560DCH4sKi2ISFn1Fa/hzoeFgjQs5jfWw8060OZMKb+VAd42F2k/9NHg7 +F4YCMQCEysXGxSznzjMsowq8sDFr3PbVWBgJJPfzty6h+ufvV/Mc5lrKq4wIHcQ2 +3547FXQ= +-----END CERTIFICATE----- +` +export const IACA_Zetes = `-----BEGIN CERTIFICATE----- +MIICQDCCAcagAwIBAgIRAJ6Pfw71u8REXgOO+UZhRhEwCgYIKoZIzj0EAwMwIjEL +MAkGA1UEBhMCQkUxEzARBgNVBAMMClRlc3QgSUEgQ0EwHhcNMjIwMzEwMjMwMDAw +WhcNMzIwMzEwMjMwMDAwWjAiMQswCQYDVQQGEwJCRTETMBEGA1UEAwwKVGVzdCBJ +QSBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMvGPCxoVAcG93w8spIbUmsJLH8E +EOxR93q/VJV/PS5qIgZpRoRKQBntqKglE5jBMAeagfCSFliK+kcaR3FwD7Oha7w+ +Ir0OZxOXSMF6wuwJDDgwqUtIhJbU6VbJGayE2KOBvzCBvDASBgNVHRMBAf8ECDAG +AQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUD5rkveRIGZGgm5ya+2/P +pVp8DTowHwYDVR0jBBgwFoAUD5rkveRIGZGgm5ya+2/PpVp8DTowPQYDVR0fBDYw +NDAyoDCgLoYsaHR0cHM6Ly96c3Bhc3MuZ2l0aHViLmlvL3pzcGFzc2VhenkvaWFj +YS5jcmwwFwYDVR0SBBAwDoEMaWFjYUB0ZXN0LmJlMAoGCCqGSM49BAMDA2gAMGUC +MQCy2pclc+GjQ8CY9L7KrXBMa8mTVOcqA1YatW2AXsYmPkL76WxAn8850UqD1XKI +v4UCMBWrVSj+VzRNZRzFaK5YAqnLEMjxdpgcZMwyVmSBJGQtPq9WqVziBX3fmBpv +WqKNog== +-----END CERTIFICATE-----` +export const IACA_OIDF = `-----BEGIN CERTIFICATE----- +MIICHjCCAcOgAwIBAgIUZX9BS5CDOJRW2t1FK1UDMt/QwMEwCgYIKoZIzj0EAwIw +ITELMAkGA1UEBhMCR0IxEjAQBgNVBAMMCU9JREYgVGVzdDAeFw0yNDExMjUwODM2 +MDRaFw0zNDExMjMwODM2MDRaMCExCzAJBgNVBAYTAkdCMRIwEAYDVQQDDAlPSURG +IFRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATT/dLsd51LLBrGV6R23o6v +ymRxHXeFBoI8yq31y5kFV2VV0gi9x5ZzEFiq8DMiAHucLACFndxLtZorCha9zznQ +o4HYMIHVMB0GA1UdDgQWBBS5cbdgAeMBi5wxpbpwISGhShAWETAfBgNVHSMEGDAW +gBS5cbdgAeMBi5wxpbpwISGhShAWETAPBgNVHRMBAf8EBTADAQH/MIGBBgNVHREE +ejB4ghB3d3cuaGVlbmFuLm1lLnVrgh1kZW1vLmNlcnRpZmljYXRpb24ub3Blbmlk +Lm5ldIIJbG9jYWxob3N0ghZsb2NhbGhvc3QuZW1vYml4LmNvLnVrgiJkZW1vLnBp +ZC1pc3N1ZXIuYnVuZGVzZHJ1Y2tlcmVpLmRlMAoGCCqGSM49BAMCA0kAMEYCIQCP +bnLxCI+WR1vhOW+A8KznAWv1MJo+YEb1MI45NKW/VQIhALzsqox8VuBRwN2dl5Lk +pnxP4oH9p6H0AOZmKP+Y7nXS +-----END CERTIFICATE-----` +export const Intermediate_IACA_Luxembourg_TST = `-----BEGIN CERTIFICATE----- +MIIC3TCCAoKgAwIBAgIUBXsqxdCHwKLia8995j6ZbjlkSigwCgYIKoZIzj0EAwIw +cjELMAkGA1UEBhMCTFUxEzARBgNVBAgMCkx1eGVtYm91cmcxEzARBgNVBAcMCkx1 +eGVtYm91cmcxDTALBgNVBAoMBENUSUUxGDAWBgNVBAsMD1BPVEVOVElBTCBHUkVF +TjEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yNTAxMjgxMTA2NDdaFw0yNTAyMjcxMTA2 +NDdaMG8xCzAJBgNVBAYTAkxVMRMwEQYDVQQIDApMdXhlbWJvdXJnMRMwEQYDVQQH +DApMdXhlbWJvdXJnMQ0wCwYDVQQKDARDVElFMRgwFgYDVQQLDA9QT1RFTlRJQUwg +R1JFRU4xDTALBgNVBAMMBENBIDEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ+ +wC55QVuli7na1lXpDHay2NtE4209BA4isxLr5+H4MjTszGuRPJZm6ncqylnHbMih +gOlGknjaf9nmrWs2p/GCo4H4MIH1MB0GA1UdDgQWBBQbKDT5TEM6WjK7yMLbwJuR +6atl7zCBrwYDVR0jBIGnMIGkgBRFbivCwHr8LtM8nFQmAoI5bvkFyqF2pHQwcjEL +MAkGA1UEBhMCTFUxEzARBgNVBAgMCkx1eGVtYm91cmcxEzARBgNVBAcMCkx1eGVt +Ym91cmcxDTALBgNVBAoMBENUSUUxGDAWBgNVBAsMD1BPVEVOVElBTCBHUkVFTjEQ +MA4GA1UEAwwHUm9vdCBDQYIUdh1IYOLqid+fbfiMWqlynOOJGOcwEgYDVR0TAQH/ +BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAgQwCgYIKoZIzj0EAwIDSQAwRgIhAKdo +XlcKmPc2LHwCdYkz/HQYqVSDnGy3FHpyaHAxSQN8AiEAkMb18EinHdhaNHu3dEC2 +kJyO4fCVxfM4dDAu+hMAfqY= +-----END CERTIFICATE----- +` +export const IACA_Nortal = `-----BEGIN CERTIFICATE----- +MIICiTCCAjCgAwIBAgIUD89WCelKX+PFzZTKvrIsMgoIjZcwCgYIKoZIzj0EAwIw +UjELMAkGA1UEBhMCRUUxDjAMBgNVBAgMBUhhcmp1MRcwFQYDVQQKDA5URVNUIEF1 +dGhvcml0eTEaMBgGA1UEAwwRVEVTVCBJQUNBIFJvb3QgQ0EwHhcNMjUwMjE5MTEx +NDI2WhcNNDUwMjE0MTExNDI2WjBSMQswCQYDVQQGEwJFRTEOMAwGA1UECAwFSGFy +anUxFzAVBgNVBAoMDlRFU1QgQXV0aG9yaXR5MRowGAYDVQQDDBFURVNUIElBQ0Eg +Um9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOJyw8+GgiTyqkzhvwA5 +nNMwsjLA24/2wjYrXJMb23fCAWeskZu014znoXZHJZK+cxlnvLtLscYCiF7REr6N +WcqjgeMwgeAwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFHAddh/lzBAlZm1hAO/QgQmhV9WBMCMGA1UdEgQcMBqBGGNvbnRhY3RA +aWFjYS5leGFtcGxlLmNvbTB2BgNVHR8EbzBtMGugaaBnhmVodHRwczovL3Jhdy5n +aXRodWJ1c2VyY29udGVudC5jb20vb3Blbi1laWQvZXVkaS1xZWFhLWlzc3Vlci1w +b2MvcmVmcy9oZWFkcy9kZXZlbG9wL2xvY2FsL2NybC9pYWNhLmNybDAKBggqhkjO +PQQDAgNHADBEAiBt84QP49zoXBFItKhxIPkg+7qXbf6eIMY0xkc1M7RYlQIgWOSU +JbP8KEreKUQ5Nwgae13YifG3jpJfU+nVWAe+1Ik= +-----END CERTIFICATE-----` +export const IACA_SpruceID = `-----BEGIN CERTIFICATE----- +MIICWTCCAf+gAwIBAgIULZgAnZswdEysOLq+G0uNW0svhYIwCgYIKoZIzj0EAwIw +VjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQKDAhTcHJ1Y2VJRDEn +MCUGA1UEAwweU3BydWNlSUQgVGVzdCBDZXJ0aWZpY2F0ZSBSb290MB4XDTI1MDIx +MjEwMjU0MFoXDTI2MDIxMjEwMjU0MFowVjELMAkGA1UEBhMCVVMxCzAJBgNVBAgM +Ak5ZMREwDwYDVQQKDAhTcHJ1Y2VJRDEnMCUGA1UEAwweU3BydWNlSUQgVGVzdCBD +ZXJ0aWZpY2F0ZSBSb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwWfpUAMW +HkOzSctR8szsMNLeOCMyjk9HAkAYZ0HiHsBMNyrOcTxScBhEiHj+trE5d5fVq36o +cvrVkt2X0yy/N6OBqjCBpzAdBgNVHQ4EFgQU+TKkY3MApIowvNzakcIr6P4ZQDQw +EgYDVR0TAQH/BAgwBgEB/wIBADA+BgNVHR8ENzA1MDOgMaAvhi1odHRwczovL2lu +dGVyb3BldmVudC5zcHJ1Y2VpZC5jb20vaW50ZXJvcC5jcmwwDgYDVR0PAQH/BAQD +AgEGMCIGA1UdEgQbMBmBF2lzb2ludGVyb3BAc3BydWNlaWQuY29tMAoGCCqGSM49 +BAMCA0gAMEUCIAJrzCSS/VIjf7uTq+Kt6+97VUNSvaAAwdP6fscIvp4RAiEA0dOP +Ld7ivuH83lLHDuNpb4NShfdBG57jNEIPNUs9OEg= +-----END CERTIFICATE----- +` +export const IACA_Lissi = `-----BEGIN CERTIFICATE----- +MIIDmjCCAoKgAwIBAgIUHBxLVMLcAwZu8PJOOmGJ1g/RrIAwDQYJKoZIhvcNAQEL +BQAwUjELMAkGA1UEBhMCREUxDzANBgNVBAcMBkJlcmxpbjEOMAwGA1UECgwFTGlz +c2kxDjAMBgNVBAsMBUxpc3NpMRIwEAYDVQQDDAlsaXNzaS1kZXYwHhcNMjUwMjI3 +MDkzNzU2WhcNMzUwMjI1MDkzNzU2WjBSMQswCQYDVQQGEwJERTEPMA0GA1UEBwwG +QmVybGluMQ4wDAYDVQQKDAVMaXNzaTEOMAwGA1UECwwFTGlzc2kxEjAQBgNVBAMM +CWxpc3NpLWRldjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKLquP3P +L2kc0gfLCx7eceaqOBnQQehnXDJPdSUwrEr59wu5bqY9LWuNEAGDwhkwHNgvJaTH +1v1QEwZ7mxfB5c9mKawoxhfL02SvzZIg7corGpjiAMXuN/2SudiFSFD3FuotRkpq +FJ2ig1H76899HgiaoJOhqjSJCpDbhcozJGtuZ+adhzURFK2h+tvkI0ghp727QHf4 +vH+P/VK7gY+YgLgaVNVAoJxZQkb2DUzMlDvkuW6/lPW7MTskhDReiVTRbx+6s4MI +xbP4lbEeu17tpFApbLG6VKWiLJ9CSLkPHCLwkTz8Hr1IU5Z2EsR3FsBPiin0ys/U +bR3XBISEhrQlcCECAwEAAaNoMGYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E +BAMCAYYwHQYDVR0OBBYEFA6KN1FoH1wqTqM8fR40bEMjArQVMCQGA1UdEQQdMBuC +GWludGVyb3AtdGVzdC5kZXYubGlzc2kuaW8wDQYJKoZIhvcNAQELBQADggEBAFrv +govuZ4dM0ZzJ4QiWqoZodqUrrtT7kDfXUyOcPtmv08DFDEQHYyg4dttFyoZVGP5r +cMkNQ+wE3E0ulvxJpAQoWVXGiYZBmy66+U75OlmRQwan/MWgyqvocUteYBKoWMkK +6iP9+9cbn40SiaFG63Ka0nmMYiKL0TuJMGmLUj7mCMIVF+qiH7LggJlRjqmnCa6w +y8XyQ+oKYp+jO76g7t0MOgYKsVpqQgnhAoeJBFhlzV9U0DrjaUPd+yZIz98pezP4 +lypb3FyM1lAhsgdTgQ4/TTADPue2VKU7GsCKB4j0pA7HRJL4dUngLMTMQ5eZKWH1 +eFW0y1sZxMSTuRHZKIQ= +-----END CERTIFICATE----- +` +export const IACA_SICPA = `-----BEGIN CERTIFICATE----- +MIIB1jCCAXugAwIBAgIUW5/kLMoKgjB8aUMjyr+CzgIZdPkwCgYIKoZIzj0EAwIw +QDELMAkGA1UEBhMCQ0gxDjAMBgNVBAoMBVNpY3BhMSEwHwYDVQQDDBhTaWNwYSBD +bG91ZCBSb290IENBIFRlc3QwHhcNMjUwMjIwMTAzOTM2WhcNNDUwOTAzMTAzOTM2 +WjBAMQswCQYDVQQGEwJDSDEOMAwGA1UECgwFU2ljcGExITAfBgNVBAMMGFNpY3Bh +IENsb3VkIFJvb3QgQ0EgVGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABG7/ +Wi2y6hbDZqUztrItJpMdQtWIMzY8gYSygTOKLageFi4JrOgP6LIW/1Mr83LfP7Of +HXCHTURyAr3X8L+z7+yjUzBRMB0GA1UdDgQWBBRLOBqyynAE84e5UYsXvjxiOVKD +tTAfBgNVHSMEGDAWgBRLOBqyynAE84e5UYsXvjxiOVKDtTAPBgNVHRMBAf8EBTAD +AQH/MAoGCCqGSM49BAMCA0kAMEYCIQDY4EUz01k86sdJTnKpsAAlMS6awZGFc03v +rybSOZSCHQIhALg7sG20h6L3VuKidRXgMqarux5jAfVhBzTbEjgusZMV +-----END CERTIFICATE----- +` +export const IACA_Idakto = `-----BEGIN CERTIFICATE----- +MIIB2TCCAX+gAwIBAgIJAI5LEB+QbO+3MAoGCCqGSM49BAMCMCUxCzAJBgNVBAYT +AkZSMRYwFAYDVQQDDA1JREFLVE8gSUFDQSA0MB4XDTI0MDkyNjEyMzQ1M1oXDTI5 +MDkyNTEyMzQ1M1owJTELMAkGA1UEBhMCRlIxFjAUBgNVBAMMDUlEQUtUTyBJQUNB +IDQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARMophaPOvIcnsKuxs3iGxJA3ky +Qx3pHeC0LWj8RNPLtYWAcPJq8bB5PSyOkGtB6XcGUTJukaBxU//5cAe2I18po4GX +MIGUMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQW +BBS2EQayXDKCQSR99d/ve3ZXwxWPpzAaBgNVHRIEEzARgQ9pYWNhQGlkYWt0by5j +b20wMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5jaG9wcGVyLmlkbGFiLmxp +dmU6NTU1NTAKBggqhkjOPQQDAgNIADBFAiAX1wM1La0uufb2yA4jZ+11FfF6hcx3 +ER8/kcC52teviQIhALNwfW4Zusu4QD8QFxRKrLyi8UVWpDXI5G7rHRnIbNPf +-----END CERTIFICATE----- +` +export const IACA_RDW_mDL = `-----BEGIN CERTIFICATE----- +MIICGjCCAcGgAwIBAgIJAKyJ/32Y8TRFMAoGCCqGSM49BAMCMD0xIDAeBgNVBAMM +F1JEVyBFVURJVyBMU1AgVEVTVCBJQUNBMQwwCgYDVQQKDANSRFcxCzAJBgNVBAYT +Ak5MMB4XDTI1MDIwNDEyMTcyNloXDTM1MDIwMjEyMTcyNlowPTEgMB4GA1UEAwwX +UkRXIEVVRElXIExTUCBURVNUIElBQ0ExDDAKBgNVBAoMA1JEVzELMAkGA1UEBhMC +TkwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATw8Y1ToKJT7xi6oGyN/YLo2/Qt +qRjrhFSmZinw5jcwdv/m5EMQaIW6dlldO9nZKnoctlX2/ctV3bMsRvxgmPd7o4Gp +MIGmMB0GA1UdDgQWBBT6rf14xVljeyMU+WRRsLYSoxJLaTAOBgNVHQ8BAf8EBAMC +AQYwGgYDVR0SBBMwEYYPaHR0cHM6Ly9yZHcubmwvMBIGA1UdEwEB/wQIMAYBAf8C +AQAwRQYDVR0fBD4wPDA6oDigNoY0aHR0cHM6Ly9tZGxpc3N1ZXIuYXp1cmV3ZWJz +aXRlcy5uZXQvY3JsL1JEV19JQUNBLmNybDAKBggqhkjOPQQDAgNHADBEAiAYs1Go +ZH5uC2JDfcaysPhAAvk+bToaVg2WU5/ssHPAFQIgMLSj3o2ebzzLBn0J6lxs8l0z +2yFhugwIYREK4ZcUALg= +-----END CERTIFICATE-----` +export const IACA_Explicit_Selection = `-----BEGIN CERTIFICATE----- +MIIClDCCAhugAwIBAgIRAM3Rb9NJiGL+XphXmqj0P0YwCgYIKoZIzj0EAwMwODEpMCcGA1UEAwwg +RVMgSWRlbnRpdHkgQ3JlZGVudGlhbCBURVNUIElBQ0ExCzAJBgNVBAYMAlpaMB4XDTI1MDIxMzE0 +NDY1NloXDTMwMDIxMzE0NDY1NlowODEpMCcGA1UEAwwgRVMgSWRlbnRpdHkgQ3JlZGVudGlhbCBU +RVNUIElBQ0ExCzAJBgNVBAYMAlpaMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEslQPJ8/RlMFK1EPj +abhKiqnxXIshFX1NwcOWydgCVPfKnDRhkAko0yO5TeeOlDq0Gj/h+X8H7LAC9/yLJJfF+iQrRRVg +i15FSN7TFbWsU2qOtCMGsznlt3wDa6Xvd1TEo4HoMIHlMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMB +Af8ECDAGAQH/AgEAMEwGA1UdEgRFMEOGQWh0dHBzOi8vZ2l0aHViLmNvbS9vcGVud2FsbGV0LWZv +dW5kYXRpb24tbGFicy9pZGVudGl0eS1jcmVkZW50aWFsMFIGA1UdHwRLMEkwR6BFoEOGQWh0dHBz +Oi8vZ2l0aHViLmNvbS9vcGVud2FsbGV0LWZvdW5kYXRpb24tbGFicy9pZGVudGl0eS1jcmVkZW50 +aWFsMB0GA1UdDgQWBBQWcvvtxAwh1zbRalsyInhiv6VR4DAKBggqhkjOPQQDAwNnADBkAjALLEYN +oRprG5T3YxCLtO6aiBuUWhuaXKKuHS6eWl1uikxGN75J01Cy223QS/qRa+kCMGf0uNU95Tor/VRp +27arE5SsKl7knFa2WnsJUeHCdx+vB5tTHwd2Fj44gePzExbA7g== +-----END CERTIFICATE-----` +export const IACA_GRNET = `-----BEGIN CERTIFICATE----- +MIICczCCAhmgAwIBAgIUJ5J7ZHMqXA9s0Q1LNWq3SLS1XYYwCgYIKoZIzj0EAwIw +PTEeMBwGA1UEAwwVUElEIElzc3VlciBDQSAtIEdSIDAxMQ4wDAYDVQQKDAVHUk5F +VDELMAkGA1UEBhMCR1IwHhcNMjUwMjExMDkzMjM1WhcNMjYwMjExMDkzMjM1WjA9 +MR4wHAYDVQQDDBVQSUQgSXNzdWVyIENBIC0gR1IgMDExDjAMBgNVBAoMBUdSTkVU +MQswCQYDVQQGEwJHUjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGT9HPUl+xEP +KG+mw8kNPgoLN1wYeNkdpy0QaUsH4kohJRaC7sWU3ZoJ0ZNlvU+jLcL8gRkRjAlg +7btmFqoStdWjgfYwgfMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUh2c5 +HNJCkeMVyPhcOHRQbWP0APYwYgYDVR0jBFswWaFBpD8wPTEeMBwGA1UEAwwVUElE +IElzc3VlciBDQSAtIEdSIDAxMQ4wDAYDVQQKDAVHUk5FVDELMAkGA1UEBhMCR1KC +FCeSe2RzKlwPbNENSzVqt0i0tV2GMBYGA1UdJQEB/wQMMAoGCCuBAgIAAAEHMDIG +A1UdHwQrMCkwJ6AloCOGIWh0dHA6Ly84My4yMTIuNzIuMTE0OjgwODIvY3JsLnBl +bTAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSAAwRQIgFakgXphlVmW3coUf +5E0dx88/LEBFpH48/F5I7iHYn3kCIQCBAhj1N5sUEqOzfx6tU+0Fn7IxM/Fb1aTC +oyINRnY8Jg== +-----END CERTIFICATE-----` +export const IACA_Diia = `-----BEGIN CERTIFICATE----- +MIIE9TCCBJqgAwIBAgIUZmkkPSsEMx0EAAAA0hFMAe/OdAMwCgYIKoZIzj0EAwIw +gdgxIDAeBgNVBAoMF1N0YXRlIGVudGVycHJpc2UgIkRJSUEiMTAwLgYDVQQLDCdE +ZXBhcnRtZW50IG9mIEVsZWN0cm9uaWMgVHJ1c3QgU2VydmljZXMxMjAwBgNVBAMM +KSJESUlBIi4gUXVhbGlmaWVkIFRydXN0IFNlcnZpY2VzIFByb3ZpZGVyMRkwFwYD +VQQFExBVQS00MzM5NTAzMy0yMzExMQswCQYDVQQGEwJVQTENMAsGA1UEBwwES3lp +djEXMBUGA1UEYQwOTlRSVUEtNDMzOTUwMzMwHhcNMjUwMjAzMDcxNjQ3WhcNMjYw +MjAzMDcxNjQ3WjCBnDEoMCYGA1UECgwfZXUuZXVyb3BhLmVjLmV1ZGkubWRsLnVh +LnRlc3QuMTEoMCYGA1UEAwwfZXUuZXVyb3BhLmVjLmV1ZGkubWRsLnVhLnRlc3Qu +MTERMA8GA1UEBRMIMjE3NjI1MTQxCzAJBgNVBAYTAlVBMQ0wCwYDVQQHDARLeWl2 +MRcwFQYDVQRhDA5OVFJVQS00MzM5NTAzMzBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABMqHBE2c1liavAy1LKXYpelI7szpssTfDoq3qElvXFOIKsVWhHoP0uMbD9nA +L0DVCSYeQzgoDHljptuhjSvNi1WjggJ6MIICdjAdBgNVHQ4EFgQU6lazY+uRh4jF +LcfDUNb8OymZREgwHwYDVR0jBBgwFoAUZmkkPSsEMx0ekqtvP0rbSCMbP8QwDgYD +VR0PAQH/BAQDAgbAMEYGA1UdIAQ/MD0wOwYJKoYkAgEBAQICMC4wLAYIKwYBBQUH +AgEWIGh0dHBzOi8vY2EuZGlpYS5nb3YudWEvcmVnbGFtZW50MAkGA1UdEwQCMAAw +PwYIKwYBBQUHAQMEMzAxMAgGBgQAjkYBATAOBgYEAI5GAQcwBBMCVUEwFQYIKwYB +BQUHCwIwCQYHBACL7EkBAjAeBgNVHREEFzAVoBMGCisGAQQBgjcUAgOgBQwDMTA4 +ME4GA1UdHwRHMEUwQ6BBoD+GPWh0dHA6Ly9jYS5kaWlhLmdvdi51YS9kb3dubG9h +ZC9jcmxzL0NBLTY2NjkyNDNELUZ1bGwtUzIxMy5jcmwwTwYDVR0uBEgwRjBEoEKg +QIY+aHR0cDovL2NhLmRpaWEuZ292LnVhL2Rvd25sb2FkL2NybHMvQ0EtNjY2OTI0 +M0QtRGVsdGEtUzIxMy5jcmwwgYcGCCsGAQUFBwEBBHsweTAwBggrBgEFBQcwAYYk +aHR0cDovL2NhLmRpaWEuZ292LnVhL3NlcnZpY2VzL29jc3AvMEUGCCsGAQUFBzAC +hjlodHRwOi8vY2EuZGlpYS5nb3YudWEvdXBsb2Fkcy9jZXJ0aWZpY2F0ZXMvZGlp +YV9lY2RzYS5wN2IwRQYIKwYBBQUHAQsEOTA3MDUGCCsGAQUFBzADhilodHRwOi8v +Y2EuZGlpYS5nb3YudWEvc2VydmljZXMvdHNwL2VjZHNhLzAKBggqhkjOPQQDAgNJ +ADBGAiEAp1EoWPSszrW/HYiBCzkcf0Y4qmrfv+21Mhk2XFIqQ2ACIQCqkFsopuuR +er1ANlijic4m/jpEnoWFqwEs8Z731ijSKA== +-----END CERTIFICATE----- +` +export const IACA_Idemia = `-----BEGIN CERTIFICATE----- +MIICgTCCAiegAwIBAgIUJutOUJNclipihThCk2Q7lMB7iDYwCgYIKoZIzj0EAwIw +VzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlNUMQ8wDQYDVQQKDAZJZGVtaWExKjAo +BgNVBAMMIVdhbGxldCBRQS1JZGVtaWEgSUFDQSBjZXJ0aWZpY2F0ZTAeFw0yNDA4 +MjEwOTQyMzVaFw0zMzA4MTkwOTQyMzVaMFcxCzAJBgNVBAYTAlVTMQswCQYDVQQI +DAJTVDEPMA0GA1UECgwGSWRlbWlhMSowKAYDVQQDDCFXYWxsZXQgUUEtSWRlbWlh +IElBQ0EgY2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ6u0P3 +FiebpEhsmh1XUmC7rMDpUEs0JpnPH9+4F2CnIWc2O5duT7WGekDiRlxuMSGNCbOt +Pfq5BDO3r/LhxMfko4HQMIHNMB0GA1UdDgQWBBR6BbGqTpxSd8UiuuQPNCxTUCJH +RDASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAhBgNVHRIEGjAY +hhZodHRwczovL3d3dy5pZGVtaWEuY29tMGUGA1UdHwReMFwwWqBYoFaGVGh0dHA6 +Ly9jcmwucmFjLXRlbmFudC5zdGFnaW5nLmlkZW50aXR5LWRldi5pZGVtaWEuaW8v +d2FsbGV0LXFhLWlkZW1pYS13YWxsZXQtY3JsLnBlbTAKBggqhkjOPQQDAgNIADBF +AiEApwZFMJ4MkB/t+kUk83Pe+gRGD12ssxDEF00ENiso33cCIHcESM6JqD27e6yP +LaEc9m9XcaVVehv4PKDl/VlLete6 +-----END CERTIFICATE-----` +export const IACA_Apple = `-----BEGIN CERTIFICATE----- +MIICATCCAaigAwIBAgIJAOP2dQgXZqJwMAoGCCqGSM49BAMCMEMxNDAyBgNVBAMM +K01vY2t0YW5hIERlcGFydG1lbnQgb2YgRHJpdmVyIFNlcnZpY2VzIElBQ0ExCzAJ +BgNVBAYTAlVTMB4XDTIyMDcyMTEyMTUxM1oXDTMwMTAwNzEyMTUxM1owQzE0MDIG +A1UEAwwrTW9ja3RhbmEgRGVwYXJ0bWVudCBvZiBEcml2ZXIgU2VydmljZXMgSUFD +QTELMAkGA1UEBhMCVVMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQsPhA9vAey +XFp3Cu7fpdi9FUF+PmdhQkYaeHXjtBiKIiHmQjWZ0dsZqu9m+SPTlLYXCVSbzsLq +b/YOx1Jo8uCUo4GEMIGBMB0GA1UdDgQWBBRU+iODoEwo4NkweSJhyAxIgdLACzAS +BgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAcBgNVHR8EFTATMBGg +D6ANhgtleGFtcGxlLmNvbTAeBgNVHRIEFzAVgRNleGFtcGxlQGV4YW1wbGUuY29t +MAoGCCqGSM49BAMCA0cAMEQCIGpl9yllp6zbqU7eZ0R5+x3vpAtOzm9kHGwvqqhi +BjBQAiB685l+lvJd6GiJvjcXo5EIowzQqv0ktOHHao8tlDrbWw== +-----END CERTIFICATE-----` +export const IACA_FeliCA = `-----BEGIN CERTIFICATE----- +MIICezCCAiGgAwIBAgIJALgcge8tKGEoMAoGCCqGSM49BAMCMHYxCzAJBgNVBAYT +AkpQMQ4wDAYDVQQIDAVKUC0xMzEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MR0wGwYD +VQQKDBRGZWxpQ2EgTmV0d29ya3MsIEluYzEhMB8GA1UEAwwYRk4gVGVzdCBDZXJ0 +aWZpY2F0ZSBJQUNBMB4XDTI0MTAwNDAzMjk0MFoXDTM0MTAwMjAzMjk0MFowdjEL +MAkGA1UEBhMCSlAxDjAMBgNVBAgMBUpQLTEzMRUwEwYDVQQHDAxEZWZhdWx0IENp +dHkxHTAbBgNVBAoMFEZlbGlDYSBOZXR3b3JrcywgSW5jMSEwHwYDVQQDDBhGTiBU +ZXN0IENlcnRpZmljYXRlIElBQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASA +I1sg71rBcvxHoiv2kbzfrLUPfBvZCHKJHcbrEabjWgRdGD4DrFhBGr1U1VnwhD98 +vIot4VbU4qwrWfKrCuyao4GXMIGUMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0O +BBYEFNmFci+h/djKirwFB9WWPUsQyOoWMA4GA1UdDwEB/wQEAwIBBjAnBgNVHRIE +IDAegRxleGFtcGxlQGZlbGljYW5ldHdvcmtzLmNvLmpwMCYGA1UdHwQfMB0wG6AZ +oBeGFWh0dHA6Ly9ub21lYW4vdGhpL3VybDAKBggqhkjOPQQDAgNIADBFAiAppXqi +NYULtAnHGVZ4YaNVYNKb0iRXENhAUUe7YxQdUAIhAMb22BkphGVDvsY2ov7wlzTs +TQuHoCnwfjAtqtftp8Vj +-----END CERTIFICATE-----` +export const IACA_HID = `-----BEGIN CERTIFICATE----- +MIICbjCCAhSgAwIBAgIJDY4vb9sj4PTLMAoGCCqGSM49BAMCMEYxCzAJBgNVBAYT +AlBIMQ4wDAYDVQQIDAVQSC0wMDELMAkGA1UECgwCREwxCzAJBgNVBAsMAlZSMQ0w +CwYDVQQDDARnb0lEMB4XDTI0MDEwOTAwMDAwMFoXDTMzMDEwOTIzNTk1OVowRjEL +MAkGA1UEBhMCUEgxDjAMBgNVBAgMBVBILTAwMQswCQYDVQQKDAJETDELMAkGA1UE +CwwCVlIxDTALBgNVBAMMBGdvSUQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQJ +8tbOXpAPblIxbmGFLI0YosOW1XgmgwgQwvn8enIENSowpyd2f6dVqL36ZNPxRtXr +ZJhTmfYo24dIiPt9RNVko4HqMIHnMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBQCwqigg1O1afapyqOvMPdUGhT9KjArBgNVHRAE +JDAigA8yMDI0MDEwOTAwMDAwMFqBDzIwMjkwMTA5MjM1OTU5WjAkBgNVHRIEHTAb +hhlodHRwOi8vd3d3LmhpZGdsb2JhbC5jb20vME8GA1UdHwRIMEYwRKBCoECGPmh0 +dHBzOi8vZGV2LWxzLmdvaWRodWIuaGlkY2xvdWQuY29tL2NpZC9oaWR0ZXN0aWFj +YW1kbC5jcmwucGVtMAoGCCqGSM49BAMCA0gAMEUCIQDCfjRXqUL1sgxqSwMqdQbU +m8qLk7kyS2iZdOvtBG8VLgIgL/7CB3UesqScGZTFTRlji1nEY/FF5/Iaqwi8M6Nm +QWk= +-----END CERTIFICATE-----` +export const IACA_Scytales = `-----BEGIN CERTIFICATE----- +MIICEjCCAbigAwIBAgIJfvZc9sFhCx1XMAoGCCqGSM49BAMCMDsxCzAJBgNVBAYT +AlNFMREwDwYDVQQKEwhTY3l0YWxlczEZMBcGA1UEAxMQU2N5dGFsZXMgUm9vdCBD +QTAeFw0yNDA5MjkxMTQ2MDBaFw0yOTA5MjkxMTQ2MDBaMDsxCzAJBgNVBAYTAlNF +MREwDwYDVQQKEwhTY3l0YWxlczEZMBcGA1UEAxMQU2N5dGFsZXMgUm9vdCBDQTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABBAh0v7pcusUp/MHss9ua62NW4eMX9m+ +dFrRT4iwlEIddTMQQWqViMpbzz0oh1dkXpqFt0fABMcLjf+7Qgtlx7+jgaQwgaEw +EgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUCA3kUHFROjqL1ETcsFn3hCsS +HrAwDgYDVR0PAQH/BAQDAgEGMCIGA1UdEgQbMBmGF2h0dHA6Ly93d3cuc2N5dGFs +ZXMuY29tMDgGA1UdHwQxMC8wLaAroCmGJ2h0dHBzOi8vc3RhdGljLm1kb2MuaWQv +Y3JsL3NjeXRhbGVzLmNybDAKBggqhkjOPQQDAgNIADBFAiADDYpH/ueYdURFX/EB +FE4s9RYx/viOjSK98iWB4W+z7AIhALai/tZtD4Ly4OuddB2pGq3zqmSlJN/RKrJg +Hxm3pZei +-----END CERTIFICATE-----` +export const IACA_Bundesdruckerei = `-----BEGIN CERTIFICATE----- +MIICCDCCAa6gAwIBAgIVAKQdR/JBR5kqO1A58qwWP8EeeedkMAoGCCqGSM49BAMC +MD0xCzAJBgNVBAYTAkRFMS4wLAYDVQQDDCVCRFIgSUFDQSBJU08vSUVDIDE4MDEz +LTUgdjEgVEVTVC1PTkxZMB4XDTIzMDUyNTEzNTgxNloXDTMzMDUyNTEzNTgxNlow +PTELMAkGA1UEBhMCREUxLjAsBgNVBAMMJUJEUiBJQUNBIElTTy9JRUMgMTgwMTMt +NSB2MSBURVNULU9OTFkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATR2+QLT2jY +0CLYfcb2D1kkGO+0702Hr6dOW1Vugo/BR0Y9O1ehY18zHXS2TKAxn4FZqu6P4Pfr +yKaqfeysimXqo4GKMIGHMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQD +AgEGMB0GA1UdEgQWMBSBEm1kbC1leGFtcGxlQGJkci5kZTAjBgNVHR8EHDAaMBig +FqAUghJtZGwuZXhhbXBsZS5iZHIuZGUwHQYDVR0OBBYEFJeKVzSI/wkjO6Af3nmD +m+EnKUBpMAoGCCqGSM49BAMCA0gAMEUCID8jhhvdybjgB9adCkQPB2e6tNPY7au5 +l/xZLQcvO4eGAiEA9naNuouhbVcHhPPRX4P1QU+e8KYpdDMntxl0jzo6Voc= +-----END CERTIFICATE-----` +export const IACA_Animo_EU_PID = `-----BEGIN CERTIFICATE----- +MIICeTCCAiCgAwIBAgIUB5E9QVZtmUYcDtCjKB/H3VQv72gwCgYIKoZIzj0EAwIwgYgxCzAJBgNVBAYTAkRFMQ8wDQYDVQQHDAZCZXJsaW4xHTAbBgNVBAoMFEJ1bmRlc2RydWNrZXJlaSBHbWJIMREwDwYDVQQLDAhUIENTIElERTE2MDQGA1UEAwwtU1BSSU5EIEZ1bmtlIEVVREkgV2FsbGV0IFByb3RvdHlwZSBJc3N1aW5nIENBMB4XDTI0MDUzMTA2NDgwOVoXDTM0MDUyOTA2NDgwOVowgYgxCzAJBgNVBAYTAkRFMQ8wDQYDVQQHDAZCZXJsaW4xHTAbBgNVBAoMFEJ1bmRlc2RydWNrZXJlaSBHbWJIMREwDwYDVQQLDAhUIENTIElERTE2MDQGA1UEAwwtU1BSSU5EIEZ1bmtlIEVVREkgV2FsbGV0IFByb3RvdHlwZSBJc3N1aW5nIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYGzdwFDnc7+Kn5ibAvCOM8ke77VQxqfMcwZL8IaIA+WCROcCfmY/giH92qMru5p/kyOivE0RC/IbdMONvDoUyaNmMGQwHQYDVR0OBBYEFNRWGMCJOOgOWIQYyXZiv6u7xZC+MB8GA1UdIwQYMBaAFNRWGMCJOOgOWIQYyXZiv6u7xZC+MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA0cAMEQCIGEm7wkZKHt/atb4MdFnXW6yrnwMUT2u136gdtl10Y6hAiBuTFqvVYth1rbxzCP0xWZHmQK9kVyxn8GPfX27EIzzsw== +-----END CERTIFICATE-----` +export const IACA_CLR_Labs_0 = `-----BEGIN CERTIFICATE----- +MIIBuTCCAV+gAwIBAgIBATAKBggqhkjOPQQDAjBEMQswCQYDVQQGEwJGUjEQMA4GA1UECgwHQ0xS +TGFiczEjMCEGA1UEAwwaQ0xSIExBQlMgSXNzdWVyIDEgSUFDQSBtREwwHhcNMjUwMTE3MTMwMzM5 +WhcNMjYwMTE3MTMxMzM5WjBEMQswCQYDVQQGEwJGUjEQMA4GA1UECgwHQ0xSTGFiczEjMCEGA1UE +AwwaQ0xSIExBQlMgSXNzdWVyIDEgSUFDQSBtREwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARY +XcqMxpvIil6xMouUqS/rIO5qWzRYzndloS96uykSReeXVDmmE5maKz9euIXMKB67AeNe5vo0Bq8l ++os1fxrQo0IwQDAdBgNVHQ4EFgQUe1uksJUz6+LnmCd8xNq40P4VbAowHwYDVR0jBBgwFoAUe1uk +sJUz6+LnmCd8xNq40P4VbAowCgYIKoZIzj0EAwIDSAAwRQIhAM5/6P55DKRJkUJr+j90tcL2poZy +1jJ4MC5jDPyFp0eNAiARPYaPj3xoD4ghu067Y6OaUqbLKB0wUVm7DYfkcp5QZg== +-----END CERTIFICATE----- +` +export const IACA_Animo_mDL = `-----BEGIN CERTIFICATE----- +MIIB8TCCAZigAwIBAgIQdDrqQwOFfVk5R4K6I0GngDAKBggqhkjOPQQDAjAdMQ4w +DAYDVQQDEwVBbmltbzELMAkGA1UEBhMCTkwwHhcNMjUwMjE4MTY0NTA1WhcNMjgw +MjE4MTY0NTA1WjAdMQ4wDAYDVQQDEwVBbmltbzELMAkGA1UEBhMCTkwwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAATcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6 +mRp2xWn9lR8juDM4hIVHgX6HmVQTPlk8mZfg4jAuv3frEuDCo4G5MIG2MEoGA1Ud +DgRDBEEE3A9V8ynqRcVjADqlfpZ9X8mwbew0TuQldH/QOpkadsVp/ZUfI7gzOISF +R4F+h5lUEz5ZPJmX4OIwLr936xLgwjAOBgNVHQ8BAf8EBAMCAQYwIQYDVR0SBBow +GIYWaHR0cHM6Ly9mdW5rZS5hbmltby5pZDASBgNVHRMBAf8ECDAGAQH/AgEAMCEG +A1UdHwQaMBgwFqAUoBKGEGh0dHBzOi8vYW5pbW8uaWQwCgYIKoZIzj0EAwIDRwAw +RAIgdsUSCK68OcYUWpwX8x71mNRbIsFvLPkA1tOvuawN5ewCICu+Svr59XdSoQGY +AR+0Zfwht1+WpatFcyMSeYde18xt +-----END CERTIFICATE-----` +export const IACA_CLR_Labs_1 = `-----BEGIN CERTIFICATE----- +MIIBuTCCAV+gAwIBAgIBATAKBggqhkjOPQQDAjBEMQswCQYDVQQGEwJGUjEQMA4GA1UECgwHQ0xS +TGFiczEjMCEGA1UEAwwaQ0xSIExBQlMgSXNzdWVyIDIgSUFDQSBtREwwHhcNMjUwMTE3MTMwMzM5 +WhcNMjYwMTE3MTMxMzM5WjBEMQswCQYDVQQGEwJGUjEQMA4GA1UECgwHQ0xSTGFiczEjMCEGA1UE +AwwaQ0xSIExBQlMgSXNzdWVyIDIgSUFDQSBtREwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQI +tq5Gc3M5h0kgwrnlAO9zri2kedXvBRs+rlSfi9xptTNIkaXOV4d9+KiQ8VJxUl25My6lF+4pxqja +O+IPbMQ/o0IwQDAdBgNVHQ4EFgQUV0IDa3ok3PC+ZLcafGc7PzM19JkwHwYDVR0jBBgwFoAUV0ID +a3ok3PC+ZLcafGc7PzM19JkwCgYIKoZIzj0EAwIDSAAwRQIgfviPOZBvsYBjOiwdmurzysOaLWBW +KoaWc88bNfIdAd0CIQC4eQojdSt1i9dfGzejkqrM/Ah/lo7kxs+ufAhTyxiCyQ== +-----END CERTIFICATE----- +` +export const IACA_COI_Poland_PID = `-----BEGIN CERTIFICATE----- +MIIDPjCCAuOgAwIBAgIUVO1MwFnWBeFW4Yl4NQUBlUaoeXMwCgYIKoZIzj0EAwIw +XjELMAkGA1UEBhMCUEwxJjAkBgNVBAoMHUNlbnRyYWxueSBPc3JvZGVrIEluZm9y +bWF0eWtpMScwJQYDVQQDDB5waWQtcHJvdmlkZXItdGVzdC1kZXZlbG9wLWxlYWYw +HhcNMjUwMjI3MDkwNjQxWhcNMjYwMjI3MDkwNjQxWjBeMQswCQYDVQQGEwJQTDEm +MCQGA1UECgwdQ2VudHJhbG55IE9zcm9kZWsgSW5mb3JtYXR5a2kxJzAlBgNVBAMM +HnBpZC1wcm92aWRlci10ZXN0LWRldmVsb3AtbGVhZjBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABCRwDpG6ZlotlFrdT5d/fa2hT0O5VVA5/AZrf3+0MMiK8YnCOiQ2 +kiOwgTB0l1berftdTaLcJCHfhVe5JerH7PSjggF9MIIBeTBSBgNVHR8ESzBJMEeg +RaBDhkFodHRwczovL2FwaS5kZXZlbG9wLmFwaS5tb2J5d2F0ZWwucHJvLmNvaS5n +b3YucGwvcGlkLXByb3ZpZGVyL2NybDBIBgNVHRIEQTA/hj1odHRwczovL2FwaS5k +ZXZlbG9wLmFwaS5tb2J5d2F0ZWwucHJvLmNvaS5nb3YucGwvcGlkLXByb3ZpZGVy +MA8GA1UdEwQIMAYBAf8CAQAwgZsGA1UdIwSBkzCBkIAUDo/OJ77IOUJYQNdTrkxj +W5cDV8ahYqRgMF4xCzAJBgNVBAYTAlBMMSYwJAYDVQQKDB1DZW50cmFsbnkgT3Ny +b2RlayBJbmZvcm1hdHlraTEnMCUGA1UEAwwecGlkLXByb3ZpZGVyLXRlc3QtZGV2 +ZWxvcC1sZWFmghRU7UzAWdYF4VbhiXg1BQGVRqh5czAdBgNVHQ4EFgQUDo/OJ77I +OUJYQNdTrkxjW5cDV8YwCwYDVR0PBAQDAgeAMAoGCCqGSM49BAMCA0kAMEYCIQCr +ZRd/2lLQ6kSx3Z7bpubgPRg/ZB4ym8QaNBfKOqUbrwIhAM1PIZAMepYkEc7TuuMn +/Eab8OsA8t3n7aU3fyDTgdbH +-----END CERTIFICATE----- + +` +export const IACA_Reaktor = `-----BEGIN CERTIFICATE----- +MIIDNDCCArqgAwIBAgIUHM6AtP9TyGFHaKLGd1aRMx7D+CkwCgYIKoZIzj0EAwMw +gagxODA2BgNVBAMML0RWViBEaWdpLUlEIE1vY2sgQXR0cmlidXRlIFNlYWxpbmcg +Q2VydGlmaWNhdGVzMSYwJAYDVQQKDB1EaWdpLSBqYSB2w6Rlc3TDtnRpZXRvdmly +YXN0bzERMA8GA1UEBwwISGVsc2lua2kxCzAJBgNVBAYTAkZJMRAwDgYDVQQIDAdG +aW5sYW5kMRIwEAYDVQQFEwkwMjQ1NDM3LTIwHhcNMjUwMjIwMDczMjEyWhcNMzQw +MjE4MDczMjEyWjCBqDE4MDYGA1UEAwwvRFZWIERpZ2ktSUQgTW9jayBBdHRyaWJ1 +dGUgU2VhbGluZyBDZXJ0aWZpY2F0ZXMxJjAkBgNVBAoMHURpZ2ktIGphIHbDpGVz +dMO2dGlldG92aXJhc3RvMREwDwYDVQQHDAhIZWxzaW5raTELMAkGA1UEBhMCRkkx +EDAOBgNVBAgMB0ZpbmxhbmQxEjAQBgNVBAUTCTAyNDU0MzctMjB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABGcz86+Pt3o1TZ5BYKXtOrFhqi6fDVq7+32J8DaeiSibJES5 +c9mAPIP/eNB8b+Wm+7RU9blUZ3xlxsmVTGScEOwwSVczmiwBHtk4+7KNltd6CrAA +FgSdyDKCEe+w+AKwLqOBojCBnzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQUk1UsWwNGTJg1yhFhQQG+2+kQ6/swGQYDVR0SBBIw +EIYOaHR0cHM6Ly9kdnYuZmkwGQYDVR0RBBIwEIYOaHR0cHM6Ly9kdnYuZmkwJAYD +VR0fBB0wGzAZoBegFYYTaHR0cHM6Ly9kdnYuaW52YWxpZDAKBggqhkjOPQQDAwNo +ADBlAjEAm6mOX8swPtAFQxmIbVu7B9E6PGSzGP+GyGMvpXiiQ6i8vB/YS61mUnf9 +tnqTthYxAjBaoY0JMfNB4/1FBr3Dl317wfzhht0L3qdGqYO+JScdzDkJViJck9YS +o3cFWT4/vv8= +-----END CERTIFICATE-----` +export const IACA_AMA = `-----BEGIN CERTIFICATE----- +MIIDHTCCAqOgAwIBAgIUVqjgtJqf4hUYJkqdYzi+0xwhwFYwCgYIKoZIzj0EAwMw +XDEeMBwGA1UEAwwVUElEIElzc3VlciBDQSAtIFVUIDAxMS0wKwYDVQQKDCRFVURJ +IFdhbGxldCBSZWZlcmVuY2UgSW1wbGVtZW50YXRpb24xCzAJBgNVBAYTAlVUMB4X +DTIzMDkwMTE4MzQxN1oXDTMyMTEyNzE4MzQxNlowXDEeMBwGA1UEAwwVUElEIElz +c3VlciBDQSAtIFVUIDAxMS0wKwYDVQQKDCRFVURJIFdhbGxldCBSZWZlcmVuY2Ug +SW1wbGVtZW50YXRpb24xCzAJBgNVBAYTAlVUMHYwEAYHKoZIzj0CAQYFK4EEACID +YgAEFg5Shfsxp5R/UFIEKS3L27dwnFhnjSgUh2btKOQEnfb3doyeqMAvBtUMlClh +sF3uefKinCw08NB31rwC+dtj6X/LE3n2C9jROIUN8PrnlLS5Qs4Rs4ZU5OIgztoa +O8G9o4IBJDCCASAwEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBSzbLiR +FxzXpBpmMYdC4YvAQMyVGzAWBgNVHSUBAf8EDDAKBggrgQICAAABBzBDBgNVHR8E +PDA6MDigNqA0hjJodHRwczovL3ByZXByb2QucGtpLmV1ZGl3LmRldi9jcmwvcGlk +X0NBX1VUXzAxLmNybDAdBgNVHQ4EFgQUs2y4kRcc16QaZjGHQuGLwEDMlRswDgYD +VR0PAQH/BAQDAgEGMF0GA1UdEgRWMFSGUmh0dHBzOi8vZ2l0aHViLmNvbS9ldS1k +aWdpdGFsLWlkZW50aXR5LXdhbGxldC9hcmNoaXRlY3R1cmUtYW5kLXJlZmVyZW5j +ZS1mcmFtZXdvcmswCgYIKoZIzj0EAwMDaAAwZQIwaXUA3j++xl/tdD76tXEWCikf +M1CaRz4vzBC7NS0wCdItKiz6HZeV8EPtNCnsfKpNAjEAqrdeKDnr5Kwf8BA7tATe +hxNlOV4Hnc10XO1XULtigCwb49RpkqlS2Hul+DpqObUs +-----END CERTIFICATE----- +` +export const IACA_Samsung = `-----BEGIN CERTIFICATE----- +MIICjzCCAjagAwIBAgIUDRLZFBspGUeIChPD5/S2gepkVSQwCgYIKoZIzj0EAwIw +YzELMAkGA1UEBhMCS1IxCzAJBgNVBAgMAlNVMSAwHgYDVQQKDBdTYW1zdW5nIEVs +ZWN0cm9uaWNzIENvLjElMCMGA1UEAwwcSUFDQSByb290IGNlcnRpZmljYXRlIChU +RVNUKTAeFw0yMzA4MTcxNDE2NDFaFw0zMjA4MTUxNDE2NDFaMGMxCzAJBgNVBAYT +AktSMQswCQYDVQQIDAJTVTEgMB4GA1UECgwXU2Ftc3VuZyBFbGVjdHJvbmljcyBD +by4xJTAjBgNVBAMMHElBQ0Egcm9vdCBjZXJ0aWZpY2F0ZSAoVEVTVCkwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAASIiZ7n4ZDdkx91XFoR+qqfTKT0074YdML4CXuT +pSMt4JAp0h1vvwBxfOt41ei8uEWFXP/1zhMAbANPF92hoRhAo4HHMIHEMB0GA1Ud +DgQWBBSFGm9gprRVNGRyYId6NX82dsNWhjAfBgNVHSMEGDAWgBSFGm9gprRVNGRy +YId6NX82dsNWhjASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAe +BgNVHRIEFzAVgRNleGFtcGxlQGV4YW1wbGUuY29tMD4GA1UdHwQ3MDUwM6AxoC+G +LWh0dHBzOi8vaW50ZXJvcGV2ZW50LnNwcnVjZWlkLmNvbS9pbnRlcm9wLmNybDAK +BggqhkjOPQQDAgNHADBEAiBWr2B5pzH2yJ6tXUKdAc8anFrshVlMgGk9h2A9u8x/ +ygIgFoBje0MMLR8ZGtNTv64gliOdbcLOG6QlHhJC+WaEn+A= +-----END CERTIFICATE-----` +export const IACA_France_Identite_Test = `-----BEGIN CERTIFICATE----- +MIICLjCCAdOgAwIBAgIUKxtI9ZwV69lv8xZUMuPRwRXFXK8wCgYIKoZIzj0EAwIw +ODELMAkGA1UEBhMCRlIxKTAnBgNVBAMMIEF0dGVzdGF0aW9uIEZyYW5jZSBJZGVu +dGl0ZSBJQUNBMB4XDTI1MDIyNjEzMTA1M1oXDTMwMDIyNTEzMTA1M1owODELMAkG +A1UEBhMCRlIxKTAnBgNVBAMMIEF0dGVzdGF0aW9uIEZyYW5jZSBJZGVudGl0ZSBJ +QUNBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEheE2xdlkm76O69GmBodzcWzH +8kavPmb7TzX9qpUNAEl9HDOWxY+/CGcBvhw4wuwvcFjIcK+qL4Lc0novwmGp56OB +ujCBtzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUX25Pm1mYnUFjwmrmsE1qorD6lj4wLgYDVR0SBCcwJYIjYXR0ZXN0YXRpb24u +ZnJhbmNlLWlkZW50aXRlLmdvdXYuZnIwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDov +L2F0dGVzdGF0aW9uLmZyYW5jZS1pZGVudGl0ZS5nb3V2LmZyL2NhLmNybDAKBggq +hkjOPQQDAgNJADBGAiEA+gau0x2yKTAjmbgCPHuS/VYMP0PlU1cfmY0z7jOqZdIC +IQCVaxM3HxbfenwK2GZ0aGBAbN16q5TUjmNvXLs2LIxNXg== +-----END CERTIFICATE----- +` +export const IACA_CLR_Labs_2 = `-----BEGIN CERTIFICATE----- +MIIBuTCCAV+gAwIBAgIBATAKBggqhkjOPQQDAjBEMQswCQYDVQQGEwJGUjEQMA4GA1UECgwHQ0xS +TGFiczEjMCEGA1UEAwwaQ0xSIExBQlMgSXNzdWVyIDMgSUFDQSBtREwwHhcNMjUwMTE3MTMwMzM5 +WhcNMjYwMTE3MTMxMzM5WjBEMQswCQYDVQQGEwJGUjEQMA4GA1UECgwHQ0xSTGFiczEjMCEGA1UE +AwwaQ0xSIExBQlMgSXNzdWVyIDMgSUFDQSBtREwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQx +lMYkN0WsX6qiMwVS+UmoQEWezPGoXifGZzem7ZIPiUfeTuEAXggb9L5olkL9tRFbJHzXxSe+yuoN +nZYFrdHLo0IwQDAdBgNVHQ4EFgQUo6mV6xAhXOjkDINlFgmppck7lwkwHwYDVR0jBBgwFoAUo6mV +6xAhXOjkDINlFgmppck7lwkwCgYIKoZIzj0EAwIDSAAwRQIgVEKRyMmL0rm8hjBqv4jo5B3cM0YT +X5Qa29WcZ7IM8i0CIQCTrRLezHklZVzUYOsNaTcbX9TgkMMMuT8sSD/5bX0Fbg== +-----END CERTIFICATE----- +` diff --git a/agent/src/issuer.ts b/agent/src/issuer.ts index 441800c..7364357 100644 --- a/agent/src/issuer.ts +++ b/agent/src/issuer.ts @@ -1,208 +1,557 @@ -import { OpenId4VciCredentialRequestToCredentialMapper } from "@credo-ts/openid4vc"; +import { ClaimFormat, JsonTransformer, W3cCredential, parseDid } from '@credo-ts/core' import { - W3cCredential, - parseDid, - KeyType, - getKeyFromVerificationMethod, -} from "@credo-ts/core"; -import { agent } from "./agent"; -import { - animoOpenId4VcPlaygroundCredentialJwtVc, - animoOpenId4VcPlaygroundCredentialLdpVc, - animoOpenId4VcPlaygroundCredentialSdJwtVcDid, - animoOpenId4VcPlaygroundCredentialSdJwtVcJwk, - credentialsSupported, - issuerDisplay, -} from "./issuerMetadata"; -import { OfferSessionMetadata } from "./session"; -import { getAvailableDids } from "./did"; -import { OpenId4VcIssuanceSessionRepository } from "@credo-ts/openid4vc/build/openid4vc-issuer/repository"; - -const issuerId = "e451c49f-1186-4fe4-816d-a942151dfd59"; - -export async function createIssuer() { - return agent.modules.openId4VcIssuer.createIssuer({ - issuerId, - credentialsSupported, - display: issuerDisplay, - }); + OpenId4VcVerifierApi, + type OpenId4VciCreateIssuerOptions, + type OpenId4VciCredentialConfigurationSupportedWithFormats, + type OpenId4VciCredentialIssuerMetadataDisplay, + type OpenId4VciCredentialRequestToCredentialMapper, + type OpenId4VciGetVerificationSessionForIssuanceSessionAuthorization, + type OpenId4VciSignMdocCredentials, + type OpenId4VciSignSdJwtCredentials, + type OpenId4VciSignW3cCredentials, +} from '@credo-ts/openid4vc' +import { agent } from './agent' +import { AGENT_HOST } from './constants' +import { getWebDidDocument } from './didWeb' +import { issuers, issuersCredentialsData } from './issuers' +import { arfCompliantPidSdJwtData, arfCompliantPidUrnVctSdJwtData, bdrIssuer } from './issuers/bdr' +import { kolnIssuer } from './issuers/koln' +import { krankenkasseIssuer } from './issuers/krankenkasse' +import { eudiPidSdJwtData, nederlandenIssuer } from './issuers/nederlanden' +import { steuernIssuer } from './issuers/steuern' +import { telOrgIssuer } from './issuers/telOrg' +import { getX509Certificates, getX509DcsCertificate } from './keyMethods' +import type { StaticLdpVcSignInput, StaticMdocSignInput, StaticSdJwtSignInput } from './types' +import { oneYearInMilliseconds, serverStartupTimeInMilliseconds, tenDaysInMilliseconds } from './utils/date' +import { getVerifier } from './verifier' +import { bundesregierungVerifier } from './verifiers/bundesregierung' +import { pidMdocCredential, pidSdJwtCredential, presentationDefinitionFromRequest } from './verifiers/util' + +export type CredentialConfigurationDisplay = NonNullable< + OpenId4VciCredentialConfigurationSupportedWithFormats['display'] +>[number] + +type IssuerDisplay = OpenId4VciCredentialIssuerMetadataDisplay & { + logo: NonNullable & { uri: string } +} + +export type MdocConfiguration = OpenId4VciCredentialConfigurationSupportedWithFormats & { + format: 'mso_mdoc' + display: [CredentialConfigurationDisplay, ...CredentialConfigurationDisplay[]] +} +export type LdpVcConfiguration = OpenId4VciCredentialConfigurationSupportedWithFormats & { + format: 'ldp_vc' + display: [CredentialConfigurationDisplay, ...CredentialConfigurationDisplay[]] } -export async function doesIssuerExist() { +export type SdJwtConfiguration = OpenId4VciCredentialConfigurationSupportedWithFormats & { + format: 'vc+sd-jwt' + display: [CredentialConfigurationDisplay, ...CredentialConfigurationDisplay[]] +} + +export interface PlaygroundIssuerOptions + extends Omit { + tags: string[] + issuerId: string + display: [IssuerDisplay, ...IssuerDisplay[]] + playgroundDisplayName?: string + credentialConfigurationsSupported: Array<{ + mso_mdoc?: { + configuration: MdocConfiguration + data: StaticMdocSignInput + } + 'vc+sd-jwt'?: { + configuration: SdJwtConfiguration + data: StaticSdJwtSignInput + } + ldp_vc?: { + configuration: LdpVcConfiguration + data: StaticLdpVcSignInput + } + }> +} + +export async function createOrUpdateIssuer(options: OpenId4VciCreateIssuerOptions & { issuerId: string }) { + if (await doesIssuerExist(options.issuerId)) { + await agent.modules.openId4VcIssuer.updateIssuerMetadata(options) + } else { + return agent.modules.openId4VcIssuer.createIssuer(options) + } +} + +export async function doesIssuerExist(issuerId: string) { try { - await agent.modules.openId4VcIssuer.getByIssuerId(issuerId); - return true; + await agent.modules.openId4VcIssuer.getIssuerByIssuerId(issuerId) + return true } catch (error) { - return false; + return false } } -export async function getIssuer() { - return agent.modules.openId4VcIssuer.getByIssuerId(issuerId); +export async function getIssuer(issuerId: string) { + return agent.modules.openId4VcIssuer.getIssuerByIssuerId(issuerId) } -export async function updateIssuer() { - await agent.modules.openId4VcIssuer.updateIssuerMetadata({ - issuerId, - credentialsSupported, - display: issuerDisplay, - }); +export function getIssuerIdForCredentialConfigurationId(credentialConfigurationId: string) { + const issuer = issuers.find(({ credentialConfigurationsSupported }) => + Object.values(credentialConfigurationsSupported) + .flatMap((a) => + Object.values(a).flatMap((b) => [ + b.data.credentialConfigurationId, + `${b.data.credentialConfigurationId}-dc-sd-jwt`, + `${b.data.credentialConfigurationId}-key-attestations`, + `${b.data.credentialConfigurationId}-dc-sd-jwt-key-attestations`, + ]) + ) + .includes(credentialConfigurationId) + ) + + if (!issuer) { + throw new Error(`Issuer not found for credential configuration id ${credentialConfigurationId}`) + } + + return issuer.issuerId } -export const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper = - async ({ - credentialsSupported, - credentialRequest, - issuanceSession, - // FIXME: it would be useful if holderBinding would include some metadata on the key type / alg used - // for the key binding - holderBinding, - }) => { - const credentialSupported = credentialsSupported[0]; - - // not sure if this check is needed anymore - if (!issuanceSession) throw new Error("Issuance session not found"); - if (!issuanceSession.issuanceMetadata) - throw new Error("No issuance metadata"); - - const { issuerDidMethod } = - issuanceSession.issuanceMetadata as unknown as OfferSessionMetadata; - - const possibleDids = getAvailableDids().filter((d) => - d.startsWith(issuerDidMethod) - ); - - let holderKeyType: KeyType; - if (holderBinding.method === "jwk") { - holderKeyType = holderBinding.jwk.keyType; - } else { - const holderDidDocument = await agent.dids.resolveDidDocument( - holderBinding.didUrl - ); - const verificationMethod = holderDidDocument.dereferenceKey( - holderBinding.didUrl - ); - holderKeyType = getKeyFromVerificationMethod(verificationMethod).keyType; +export const getVerificationSessionForIssuanceSession: OpenId4VciGetVerificationSessionForIssuanceSessionAuthorization = + async ({ agentContext, scopes, requestedCredentialConfigurations }) => { + const verifier = await getVerifier(bundesregierungVerifier.verifierId) + const certificates = getX509Certificates() + const verifierApi = agentContext.dependencyManager.resolve(OpenId4VcVerifierApi) + + const [credentialConfigurationId, credentialConfiguration] = Object.entries(requestedCredentialConfigurations)[0] + + if (credentialConfiguration.format !== 'mso_mdoc' && credentialConfiguration.format !== 'vc+sd-jwt') { + throw new Error('Presentation during issuance is only supported for mso_mdoc and vc+sd-jwt') } - if (possibleDids.length === 0) { - throw new Error("No available DIDs for the issuer method"); + const credentialName = credentialConfiguration.display?.[0]?.name ?? 'card' + + const authorizationRequest = await verifierApi.createAuthorizationRequest({ + verifierId: verifier.verifierId, + requestSigner: { + method: 'x5c', + x5c: certificates, + }, + presentationExchange: + credentialConfigurationId === arfCompliantPidSdJwtData.credentialConfigurationId || + credentialConfigurationId === `${arfCompliantPidSdJwtData.credentialConfigurationId}-dc-sd-jwt` || + credentialConfigurationId === + `${arfCompliantPidSdJwtData.credentialConfigurationId}-dc-sd-jwt-key-attestations` || + credentialConfigurationId === `${arfCompliantPidSdJwtData.credentialConfigurationId}-key-attestations` || + credentialConfigurationId === arfCompliantPidUrnVctSdJwtData.credentialConfigurationId || + credentialConfigurationId === `${arfCompliantPidUrnVctSdJwtData.credentialConfigurationId}-dc-sd-jwt` || + credentialConfigurationId === + `${arfCompliantPidUrnVctSdJwtData.credentialConfigurationId}-dc-sd-jwt-key-attestations` || + credentialConfigurationId === `${arfCompliantPidUrnVctSdJwtData.credentialConfigurationId}-key-attestations` || + credentialConfigurationId === eudiPidSdJwtData.credentialConfigurationId || + credentialConfigurationId === `${eudiPidSdJwtData.credentialConfigurationId}-dc-sd-jwt` || + credentialConfigurationId === `${eudiPidSdJwtData.credentialConfigurationId}-dc-sd-jwt-key-attestaions` || + credentialConfigurationId === `${eudiPidSdJwtData.credentialConfigurationId}-key-attestations` + ? { + definition: presentationDefinitionFromRequest({ + name: 'Identity card', + purpose: 'To issue your ARF compliant PID we need to verify your german PID', + credentials: [ + pidSdJwtCredential({ + fields: [ + 'issuing_country', + 'issuing_authority', + 'family_name', + 'birthdate', + 'age_birth_year', + 'age_in_years', + 'given_name', + 'birth_family_name', + 'place_of_birth.locality', + 'address.country', + 'address.postal_code', + 'address.locality', + 'address.street_address', + 'age_equal_or_over.12', + 'age_equal_or_over.14', + 'age_equal_or_over.16', + 'age_equal_or_over.18', + 'age_equal_or_over.21', + 'age_equal_or_over.65', + 'nationalities', + ], + }), + ], + }), + } + : { + definition: presentationDefinitionFromRequest({ + name: 'Identity card', + purpose: `To issue your ${credentialName} we need to verify your identity card`, + credentials: [ + pidSdJwtCredential({ + fields: [ + 'given_name', + 'family_name', + 'birthdate', + 'issuing_authority', + 'issuing_country', + 'address', + 'place_of_birth', + 'nationalities', + ], + }), + pidMdocCredential({ + fields: [ + 'given_name', + 'family_name', + 'birth_date', + 'issuing_country', + 'issuing_authority', + 'resident_street', + 'resident_postal_code', + 'resident_city', + 'birth_place', + 'nationality', + ], + }), + ], + credential_sets: [[0, 1]], + }), + }, + responseMode: 'direct_post.jwt', + }) + + return { + ...authorizationRequest, + scopes, } + } - let issuerDidUrl: string | undefined = undefined; +export const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper = async ({ + holderBinding, + credentialConfigurationId, + verification, + issuanceSession, +}): Promise => { + const certificates = getX509Certificates() - for (const possibleDid of possibleDids) { - const didDocument = await agent.dids.resolveDidDocument(possibleDid); - // Set the first verificationMethod as backup, in case we won't find a match - if (!issuerDidUrl && didDocument.verificationMethod?.[0].id) { - issuerDidUrl = didDocument.verificationMethod?.[0].id; - } + const normalizedCredentialConfigurationId = credentialConfigurationId + .replace('-dc-sd-jwt', '') + .replace('-key-attestations', '') + const credentialData = issuersCredentialsData[normalizedCredentialConfigurationId] + if (!credentialData) { + throw new Error(`Unsupported credential configuration id ${credentialConfigurationId}`) + } - const matchingVerificationMethod = didDocument.assertionMethod?.find( - (assertionMethod) => { - const verificationMethod = - typeof assertionMethod === "string" - ? didDocument.dereferenceVerificationMethod(assertionMethod) - : assertionMethod; - const keyType = - getKeyFromVerificationMethod(verificationMethod).keyType; - return keyType === holderKeyType; - } - ); - - if (matchingVerificationMethod) { - issuerDidUrl = - typeof matchingVerificationMethod === "string" - ? matchingVerificationMethod - : matchingVerificationMethod.id; - break; + if (issuanceSession.presentation?.required) { + const descriptor = verification?.presentationExchange?.descriptors[0] + + // We allow receiving the PID in both SD-JWT and mdoc when issuing in sd-jwt or mdoc format + if (descriptor?.claimFormat === ClaimFormat.SdJwtVc || descriptor?.claimFormat === ClaimFormat.MsoMdoc) { + const driversLicenseClaims = + descriptor.claimFormat === ClaimFormat.SdJwtVc + ? { + given_name: descriptor.credential.prettyClaims.given_name, + family_name: descriptor.credential.prettyClaims.family_name, + birth_date: descriptor.credential.prettyClaims.birthdate, + + issuing_authority: descriptor.credential.prettyClaims.issuing_authority, + + // NOTE: MUST be same as the C= value in the issuer cert for mdoc (checked by libs) + // We can request PID SD-JWT and issue mDOC drivers license, so to make it easier we + // always set it + issuing_country: 'NL', + // issuing_country: descriptor.credential.prettyClaims.issuing_country, + } + : { + given_name: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].given_name, + family_name: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].family_name, + birth_date: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].birth_date, + + issuing_authority: + descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].issuing_authority, + + // NOTE: MUST be same as the C= value in the issuer cert for mdoc (checked by libs) + issuing_country: 'NL', + // issuing_country: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].issuing_country, + } + + const taxIdClaims = + descriptor.claimFormat === ClaimFormat.SdJwtVc + ? { + registered_given_name: descriptor.credential.prettyClaims.given_name, + registered_family_name: descriptor.credential.prettyClaims.family_name, + resident_address: `${(descriptor.credential.prettyClaims.address as Record).street_address}, ${(descriptor.credential.prettyClaims.address as Record).postal_code} ${(descriptor.credential.prettyClaims.address as Record).locality}`, + birth_date: descriptor.credential.prettyClaims.birthdate, + + issuing_authority: descriptor.credential.prettyClaims.issuing_authority, + issuing_country: descriptor.credential.prettyClaims.issuing_country, + } + : { + registered_given_name: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].given_name, + registered_family_name: + descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].family_name, + resident_address: `${descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].resident_street}, ${descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].resident_postal_code} ${descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].resident_city}`, + birth_date: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].birth_date, + issuing_authority: + descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].issuing_authority, + issuing_country: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].issuing_country, + } + + const certificateOfResidenceClaims = + descriptor.claimFormat === ClaimFormat.SdJwtVc + ? { + family_name: descriptor.credential.prettyClaims.family_name, + given_name: descriptor.credential.prettyClaims.given_name, + resident_address: `${(descriptor.credential.prettyClaims.address as Record).street_address}, ${(descriptor.credential.prettyClaims.address as Record).postal_code} ${(descriptor.credential.prettyClaims.address as Record).locality}`, + birth_date: descriptor.credential.prettyClaims.birthdate, + birth_place: (descriptor.credential.prettyClaims.place_of_birth as Record).locality, + nationality: (descriptor.credential.prettyClaims.nationalities as string[])[0], + issuing_country: descriptor.credential.prettyClaims.issuing_country, + } + : { + given_name: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].given_name, + family_name: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].family_name, + resident_address: `${descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].resident_street}, ${descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].resident_postal_code} ${descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].resident_city}`, + birth_date: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].birth_date, + birth_place: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].birth_place, + nationality: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].nationality, + issuing_country: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].issuing_country, + } + + const healthIdClaims = descriptor.claimFormat === ClaimFormat.SdJwtVc ? {} : {} + + const msisdnClaimsData = + descriptor.claimFormat === ClaimFormat.SdJwtVc + ? { + registered_given_name: descriptor.credential.prettyClaims.given_name, + registered_family_name: descriptor.credential.prettyClaims.family_name, + } + : { + registered_given_name: descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].given_name, + registered_family_name: + descriptor.credential.issuerSignedNamespaces['eu.europa.ec.eudi.pid.1'].family_name, + } + + const arfCompliantPidData = + descriptor.claimFormat === ClaimFormat.SdJwtVc + ? { + family_name: descriptor.credential.prettyClaims.family_name, + given_name: descriptor.credential.prettyClaims.given_name, + birth_date: descriptor.credential.prettyClaims.birthdate, + age_over_18: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['18'], + + // Mandatory metadata + issuance_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), + issuing_country: descriptor.credential.prettyClaims.issuing_country, + issuing_authority: descriptor.credential.prettyClaims.issuing_authority, + + // Optional: + age_over_12: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['12'], + age_over_14: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['14'], + age_over_16: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['16'], + age_over_21: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['21'], + age_over_65: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['65'], + age_in_years: descriptor.credential.prettyClaims.age_in_years, + age_birth_year: descriptor.credential.prettyClaims.age_birth_year, + family_name_birth: descriptor.credential.prettyClaims.birth_family_name, + + birth_place: (descriptor.credential.prettyClaims.place_of_birth as Record).locality, + + resident_country: (descriptor.credential.prettyClaims.address as Record).country, + resident_city: (descriptor.credential.prettyClaims.address as Record).locality, + resident_postal_code: (descriptor.credential.prettyClaims.address as Record).postal_code, + resident_street: (descriptor.credential.prettyClaims.address as Record).street_address, + nationality: (descriptor.credential.prettyClaims.nationalities as string[])[0], + } + : {} + + const nederlandenPidData = + descriptor.claimFormat === ClaimFormat.SdJwtVc + ? { + family_name: descriptor.credential.prettyClaims.family_name, + given_name: descriptor.credential.prettyClaims.given_name, + birth_date: descriptor.credential.prettyClaims.birthdate, + age_over_18: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['18'], + + // Mandatory metadata + issuance_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), + issuing_country: descriptor.credential.prettyClaims.issuing_country, + issuing_authority: descriptor.credential.prettyClaims.issuing_authority, + + sex: descriptor.credential.prettyClaims.sex, + + // Optional: + age_over_12: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['12'], + age_over_14: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['14'], + age_over_16: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['16'], + age_over_21: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['21'], + age_over_65: (descriptor.credential.prettyClaims.age_equal_or_over as Record)['65'], + age_in_years: descriptor.credential.prettyClaims.age_in_years, + age_birth_year: descriptor.credential.prettyClaims.age_birth_year, + family_name_birth: descriptor.credential.prettyClaims.birth_family_name, + + birth_place: (descriptor.credential.prettyClaims.place_of_birth as Record).locality, + + resident_country: (descriptor.credential.prettyClaims.address as Record).country, + resident_city: (descriptor.credential.prettyClaims.address as Record).locality, + resident_postal_code: (descriptor.credential.prettyClaims.address as Record).postal_code, + resident_street: (descriptor.credential.prettyClaims.address as Record).street_address, + nationality: (descriptor.credential.prettyClaims.nationalities as string[])[0], + } + : {} + + const formatSpecificClaims = Object.fromEntries( + Object.entries({ + [bdrIssuer.credentialConfigurationsSupported[0]['vc+sd-jwt'].data.credentialConfigurationId]: + driversLicenseClaims, + [bdrIssuer.credentialConfigurationsSupported[0].mso_mdoc.data.credentialConfigurationId]: + driversLicenseClaims, + + [bdrIssuer.credentialConfigurationsSupported[1]['vc+sd-jwt'].data.credentialConfigurationId]: + arfCompliantPidData, + + [bdrIssuer.credentialConfigurationsSupported[2]['vc+sd-jwt'].data.credentialConfigurationId]: + arfCompliantPidData, + + [nederlandenIssuer.credentialConfigurationsSupported[0].mso_mdoc.data.credentialConfigurationId]: + nederlandenPidData, + // @ts-expect-error Can be undefined because other configuration does not have vc+sd-jwt + [nederlandenIssuer.credentialConfigurationsSupported[0]['vc+sd-jwt'].data.credentialConfigurationId]: + nederlandenPidData, + + [krankenkasseIssuer.credentialConfigurationsSupported[0]['vc+sd-jwt'].data.credentialConfigurationId]: + healthIdClaims, + [krankenkasseIssuer.credentialConfigurationsSupported[0].mso_mdoc.data.credentialConfigurationId]: + healthIdClaims, + + [steuernIssuer.credentialConfigurationsSupported[0]['vc+sd-jwt'].data.credentialConfigurationId]: taxIdClaims, + [steuernIssuer.credentialConfigurationsSupported[0].mso_mdoc.data.credentialConfigurationId]: taxIdClaims, + + [kolnIssuer.credentialConfigurationsSupported[0]['vc+sd-jwt'].data.credentialConfigurationId]: + certificateOfResidenceClaims, + [kolnIssuer.credentialConfigurationsSupported[0].mso_mdoc.data.credentialConfigurationId]: + certificateOfResidenceClaims, + + [telOrgIssuer.credentialConfigurationsSupported[0]['vc+sd-jwt'].data.credentialConfigurationId]: + msisdnClaimsData, + [telOrgIssuer.credentialConfigurationsSupported[0].mso_mdoc.data.credentialConfigurationId]: msisdnClaimsData, + }).flatMap(([configurationId, data]) => [ + [configurationId, data], + [`${configurationId}-key-attestations`, data], + [`${configurationId}-dc-sd-jwt`, data], + [`${configurationId}-dc-sd-jwt-key-attestations`, data], + ]) + ) + + if (credentialData.format === ClaimFormat.SdJwtVc) { + const { credential, ...restCredentialData } = credentialData + + return { + ...restCredentialData, + credentials: holderBinding.keys.map((holderBinding) => ({ + ...credential, + payload: { + ...credential.payload, + ...formatSpecificClaims[credentialConfigurationId], + }, + holder: holderBinding, + issuer: { + method: 'x5c', + x5c: certificates, + issuer: AGENT_HOST, + }, + })), + } satisfies OpenId4VciSignSdJwtCredentials } - } - if (!issuerDidUrl) { - throw new Error("No matching verification method found"); + if (credentialData.format === ClaimFormat.MsoMdoc) { + const { credential, ...restCredentialData } = credentialData + + const [namespace, values] = Object.entries(credential.namespaces)[0] + console.log({ + ...restCredentialData, + credentials: holderBinding.keys.map((holderBinding) => ({ + ...credential, + namespaces: { + [namespace]: { + ...values, + ...formatSpecificClaims[credentialConfigurationId], + }, + }, + + holderKey: holderBinding.key, + issuerCertificate: getX509DcsCertificate(), + })), + }) + return { + ...restCredentialData, + credentials: holderBinding.keys.map((holderBinding) => ({ + ...credential, + namespaces: { + [namespace]: { + ...values, + ...formatSpecificClaims[credentialConfigurationId], + }, + }, + + holderKey: holderBinding.key, + issuerCertificate: getX509DcsCertificate(), + })), + } satisfies OpenId4VciSignMdocCredentials + } } + } - if ( - credentialSupported.format === "vc+sd-jwt" && - (credentialSupported.id === - animoOpenId4VcPlaygroundCredentialSdJwtVcDid.id || - credentialSupported.id === - animoOpenId4VcPlaygroundCredentialSdJwtVcJwk.id) - ) { - return { - format: "vc+sd-jwt", + if (credentialData.format === ClaimFormat.SdJwtVc) { + const { credential, ...restCredentialData } = credentialData + return { + ...restCredentialData, + credentials: holderBinding.keys.map((holderBinding) => ({ + ...credential, holder: holderBinding, - payload: { - vct: credentialSupported.vct, - - playground: { - framework: "Aries Framework JavaScript", - language: "TypeScript", - version: "1.0", - createdBy: "Animo Solutions", - }, - }, issuer: { - didUrl: issuerDidUrl, - method: "did", + method: 'x5c', + x5c: certificates, + issuer: AGENT_HOST, }, - disclosureFrame: { - playground: { - language: true, - version: true, - }, - } as any, - }; - } + })), + } satisfies OpenId4VciSignSdJwtCredentials + } - if ( - (credentialSupported.format === "jwt_vc_json" && - credentialSupported.id === - animoOpenId4VcPlaygroundCredentialJwtVc.id) || - (credentialSupported.format === "ldp_vc" && - credentialSupported.id === animoOpenId4VcPlaygroundCredentialLdpVc.id) - ) { - if (holderBinding.method !== "did") { - throw new Error("Only did holder binding supported for JWT VC"); - } - return { - format: - credentialSupported.format === "jwt_vc_json" ? "jwt_vc" : "ldp_vc", - verificationMethod: issuerDidUrl, - credential: W3cCredential.fromJson({ - // FIXME: we need to include/cache default contexts in AFJ - // It quite slow the first time now - // And not secure - "@context": - credentialSupported.format === "ldp_vc" - ? [ - "https://www.w3.org/2018/credentials/v1", - // Fields must be defined for JSON-LD - { - "@vocab": - "https://www.w3.org/ns/credentials/issuer-dependent#", - }, - ] - : ["https://www.w3.org/2018/credentials/v1"], - // TODO: should 'VerifiableCredential' be in the issuer metadata type? - // FIXME: jwt verification did not fail when this was array within array - // W3cCredential is not validated in AFJ??? - type: ["VerifiableCredential", ...credentialSupported.types], - issuanceDate: new Date().toISOString(), - issuer: parseDid(issuerDidUrl).did, - credentialSubject: { - id: parseDid(holderBinding.didUrl).did, - playground: { - framework: "Aries Framework JavaScript", - language: "TypeScript", - version: "1.0", - createdBy: "Animo Solutions", - }, - }, - }), - }; - } + if (credentialData.format === ClaimFormat.MsoMdoc) { + const { credential, ...restCredentialData } = credentialData + return { + ...restCredentialData, + credentials: holderBinding.keys.map((holderBinding) => ({ + ...credential, + holderKey: holderBinding.key, + issuerCertificate: getX509DcsCertificate(), + })), + } satisfies OpenId4VciSignMdocCredentials + } + + if (credentialData.format === ClaimFormat.LdpVc) { + const { credential, ...restCredentialData } = credentialData + + const didWeb = await getWebDidDocument() + return { + ...restCredentialData, + credentials: holderBinding.keys.map((holderBinding) => { + if (holderBinding.method !== 'did') { + throw new Error("Only 'did' holder binding supported for ldp vc") + } - throw new Error(`Unsupported credential ${credentialSupported.id}`); - }; + const json = JsonTransformer.toJSON(credential.credential) + json.credentialSubject.id = parseDid(holderBinding.didUrl).did + json.issuer.id = didWeb.id + + return { + verificationMethod: `${didWeb.id}#key-1`, + credential: W3cCredential.fromJson(json), + } + }), + } satisfies OpenId4VciSignW3cCredentials + } + + throw new Error(`Unsupported credential ${credentialConfigurationId}`) +} diff --git a/agent/src/issuerMetadata.ts b/agent/src/issuerMetadata.ts deleted file mode 100644 index 13ab2a1..0000000 --- a/agent/src/issuerMetadata.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { JwaSignatureAlgorithm } from "@credo-ts/core"; -import { - OpenId4VciCredentialSupportedWithId, - OpenId4VciCredentialFormatProfile, - OpenId4VciIssuerMetadataDisplay, -} from "@credo-ts/openid4vc"; - -const ANIMO_BLUE = "#5e7db6"; -const ANIMO_RED = "#E17471"; -const ANIMO_DARK_BACKGROUND = "#202223"; -const WHITE = "#FFFFFF"; - -export const issuerDisplay = [ - { - background_color: ANIMO_DARK_BACKGROUND, - name: "Animo OpenID4VC Playground", - locale: "en", - logo: { - alt_text: "Animo logo", - url: "https://i.imgur.com/PUAIUed.jpeg", - }, - text_color: WHITE, - }, -] satisfies OpenId4VciIssuerMetadataDisplay[]; - -export const animoOpenId4VcPlaygroundCredentialSdJwtVcDid = { - id: "AnimoOpenId4VcPlaygroundSdJwtVcDid", - format: OpenId4VciCredentialFormatProfile.SdJwtVc, - vct: "AnimoOpenId4VcPlayground", - cryptographic_binding_methods_supported: ["did:key", "did:jwk"], - cryptographic_suites_supported: [ - JwaSignatureAlgorithm.EdDSA, - JwaSignatureAlgorithm.ES256, - ], - display: [ - { - name: "SD-JWT-VC", - description: "DID holder binding", - background_color: ANIMO_DARK_BACKGROUND, - locale: "en", - text_color: WHITE, - }, - ], -} as const satisfies OpenId4VciCredentialSupportedWithId; - -export const animoOpenId4VcPlaygroundCredentialSdJwtVcJwk = { - id: "AnimoOpenId4VcPlaygroundSdJwtVcJwk", - format: OpenId4VciCredentialFormatProfile.SdJwtVc, - vct: "AnimoOpenId4VcPlayground", - cryptographic_binding_methods_supported: ["jwk"], - cryptographic_suites_supported: [ - JwaSignatureAlgorithm.EdDSA, - JwaSignatureAlgorithm.ES256, - ], - display: [ - { - name: "SD-JWT-VC", - description: "JWK holder binding", - background_color: ANIMO_DARK_BACKGROUND, - locale: "en", - text_color: WHITE, - }, - ], -} as const satisfies OpenId4VciCredentialSupportedWithId; - -export const animoOpenId4VcPlaygroundCredentialJwtVc = { - id: "AnimoOpenId4VcPlaygroundJwtVc", - format: OpenId4VciCredentialFormatProfile.JwtVcJson, - types: ["AnimoOpenId4VcPlayground"], - cryptographic_binding_methods_supported: ["did:key", "did:jwk"], - cryptographic_suites_supported: [ - JwaSignatureAlgorithm.EdDSA, - JwaSignatureAlgorithm.ES256, - ], - display: [ - { - name: "JWT VC", - background_color: ANIMO_DARK_BACKGROUND, - locale: "en", - text_color: WHITE, - }, - ], -} as const satisfies OpenId4VciCredentialSupportedWithId; - -export const animoOpenId4VcPlaygroundCredentialLdpVc = { - id: "AnimoOpenId4VcPlaygroundLdpVc", - format: OpenId4VciCredentialFormatProfile.LdpVc, - types: ["AnimoOpenId4VcPlayground"], - "@context": ["https://www.w3.org/2018/credentials/v1"], - cryptographic_binding_methods_supported: ["did:key", "did:jwk"], - cryptographic_suites_supported: [ - JwaSignatureAlgorithm.EdDSA, - // TODO: is it needed that proof type is added here? - // According to spec yes, but it's only used for the request proof, - // which is a jwt/cwt (so alg) - "Ed25519Signature2018", - ], - display: [ - { - name: "LDP VC", - background_color: ANIMO_DARK_BACKGROUND, - locale: "en", - text_color: WHITE, - }, - ], -} as const satisfies OpenId4VciCredentialSupportedWithId; - -export const credentialsSupported = [ - animoOpenId4VcPlaygroundCredentialSdJwtVcDid, - animoOpenId4VcPlaygroundCredentialSdJwtVcJwk, - animoOpenId4VcPlaygroundCredentialJwtVc, - // Not really working yet - // FIXME: Ed25519Signature2018 required ed25519 context url - // but that is bullshit, as you can just use another verification - // method to issue/verify such as JsonWebKey or MultiKey - // animoOpenId4VcPlaygroundCredentialLdpVc, -] as const satisfies OpenId4VciCredentialSupportedWithId[]; -type CredentialSupportedId = (typeof credentialsSupported)[number]["id"]; -export const credentialSupportedIds = credentialsSupported.map((s) => s.id) as [ - CredentialSupportedId, - ...CredentialSupportedId[] -]; diff --git a/agent/src/issuers/bdr.ts b/agent/src/issuers/bdr.ts new file mode 100644 index 0000000..b4e0cd1 --- /dev/null +++ b/agent/src/issuers/bdr.ts @@ -0,0 +1,449 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm, W3cCredential } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' + +import { AGENT_HOST } from '../constants' +import type { + CredentialConfigurationDisplay, + LdpVcConfiguration, + MdocConfiguration, + PlaygroundIssuerOptions, + SdJwtConfiguration, +} from '../issuer' +import type { StaticLdpVcSignInput, StaticMdocSignInput, StaticSdJwtSignInput } from '../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../utils/date' +import { loadJPEGBufferSync } from '../utils/image' + +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../assets/erika.jpeg`) +const erikaSignature = loadJPEGBufferSync(`${__dirname}/../../assets/signature.jpeg`) + +const mobileDriversLicenseDisplay = { + locale: 'en', + name: 'Drivers Licence', + text_color: '#6F5C77', + background_color: '#E6E2E7', + background_image: { + url: `${AGENT_HOST}/assets/issuers/bdr/credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const mobileDriversLicensePayload = { + given_name: 'Erika', + family_name: 'Mustermann', + birth_date: new DateOnly('1964-08-12'), + age_over_18: true, + document_number: 'Z021AB37X13', + portrait: new Uint8Array(erikaPortrait), + signature_usual_mark: new Uint8Array(erikaSignature), + un_distinguishing_sign: 'D', + issuing_authority: 'Bundesrepublik Deutschland', + issue_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), + // Must be same as C= in x509 cert (currently set to NL) + issuing_country: 'NL', + driving_privileges: [ + { + vehicle_category_code: 'B', + issue_date: new DateOnly('2024-01-15'), + expiry_date: new DateOnly('2039-01-14'), + codes: [ + { + code: 'B96', + value: '4250', + sign: '≤', + }, + { + code: '70', + value: '01.01', + }, + { + code: '95', + value: '2029-01-15', + }, + { + code: '96', + value: '750', + sign: '≤', + }, + ], + }, + ], +} + +export const mobileDriversLicenseMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'mobile-drivers-license-mdoc', + doctype: 'org.iso.18013.5.1.mDL', + display: [mobileDriversLicenseDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const mobileDriversLicenseLdpVc = { + format: OpenId4VciCredentialFormatProfile.LdpVc, + cryptographic_binding_methods_supported: ['did:jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'mobile-drivers-license-ldp-vc', + credential_definition: { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vdl/v2'], + type: ['VerifiableCredential', 'Iso18013DriversLicenseCredential'], + }, + display: [mobileDriversLicenseDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.EdDSA, 'Ed25519Signature2020'], + }, + }, +} as const satisfies LdpVcConfiguration + +export const mobileDriversLicenseLdpVcData = { + credentialConfigurationId: 'mobile-drivers-license-ldp-vc', + format: ClaimFormat.LdpVc, + credential: { + credential: W3cCredential.fromJson({ + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/vdl/v2'], + type: ['VerifiableCredential', 'Iso18013DriversLicenseCredential'], + issuer: { + id: 'did:key:z6MkjxvA4FNrQUhr8f7xhdQuP1VPzErkcnfxsRaU5oFgy2E5', + name: 'Bundesdruckerei', + image: `${AGENT_HOST}/assets/issuers/bdr/issuer.png`, + }, + issuanceDate: '2023-11-15T10:00:00-07:00', + expirationDate: '2028-11-15T12:00:00-06:00', + name: "Utopia Driver's License", + image: 'data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUg...kSuQmCC', + description: 'A license granting driving privileges in Utopia.', + credentialSubject: { + id: 'did:example:12347abcd', + type: 'LicensedDriver', + driversLicense: { + type: 'Iso18013DriversLicense', + document_number: '542426814', + family_name: 'TURNER', + given_name: 'SUSAN', + portrait: 'data:image/jpeg;base64,/9j/4AAQSkZJR...RSClooooP/2Q==', + birth_date: '1998-08-28', + issue_date: '2023-01-15T10:00:00-07:00', + expiry_date: '2028-08-27T12:00:00-06:00', + issuing_country: 'NL', + issuing_authority: 'NL', + driving_privileges: [ + { + codes: [{ code: 'D' }], + vehicle_category_code: 'D', + issue_date: '2019-01-01', + expiry_date: '2027-01-01', + }, + { + codes: [{ code: 'C' }], + vehicle_category_code: 'C', + issue_date: '2019-01-01', + expiry_date: '2017-01-01', + }, + ], + un_distinguishing_sign: 'UTA', + sex: 2, + }, + }, + }), + }, +} satisfies StaticLdpVcSignInput + +export const mobileDriversLicenseMdocData = { + credentialConfigurationId: 'mobile-drivers-license-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: mobileDriversLicenseMdoc.doctype, + namespaces: { + 'org.iso.18013.5.1': mobileDriversLicensePayload, + }, + validityInfo: { + validFrom: mobileDriversLicensePayload.issue_date, + validUntil: mobileDriversLicensePayload.expiry_date, + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: mobileDriversLicensePayload.issue_date, + }, + }, +} satisfies StaticMdocSignInput + +export const mobileDriversLicenseSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'mobile-drivers-license-sd-jwt', + vct: 'https://example.eudi.ec.europa.eu/mdl/1', + display: [mobileDriversLicenseDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies SdJwtConfiguration + +export const mobileDriversLicenseSdJwtData = { + credentialConfigurationId: 'mobile-drivers-license-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + ...mobileDriversLicensePayload, + birth_date: mobileDriversLicensePayload.birth_date.toISOString(), + nbf: dateToSeconds(mobileDriversLicensePayload.issue_date), + exp: dateToSeconds(mobileDriversLicensePayload.expiry_date), + issue_date: mobileDriversLicensePayload.issue_date.toISOString(), + expiry_date: mobileDriversLicensePayload.expiry_date.toISOString(), + vct: mobileDriversLicenseSdJwt.vct, + portrait: `data:image/jpeg;base64,${erikaPortrait.toString('base64')}`, + signature_usual_mark: `data:image/jpeg;base64,${erikaSignature.toString('base64')}`, + driving_privileges: [ + { + ...mobileDriversLicensePayload.driving_privileges[0], + issue_date: mobileDriversLicensePayload.driving_privileges[0].issue_date.toISOString(), + expiry_date: mobileDriversLicensePayload.driving_privileges[0].expiry_date.toISOString(), + }, + ], + }, + disclosureFrame: { + _sd: [ + 'given_name', + 'family_name', + 'birth_date', + 'document_number', + 'portrait', + 'un_distinguishing_sign', + 'issuing_authority', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'driving_privileges', + 'signature_usual_mark', + 'age_over_18', + ], + // TODO: fix array disclosures? + // @ts-ignore + // driving_privileges: mobileDriversLicensePayload.driving_privileges.map((d) => ({ + // _sd: ['vehicle_category_code', 'issue_date', 'expiry_date', 'codes'], + // })), + }, + }, +} satisfies StaticSdJwtSignInput + +const arfCompliantPidDisplay = { + locale: 'en', + name: 'PID (ARF)', + text_color: '#2F3544', + background_color: '#F1F2F0', + background_image: { + url: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const arfCompliantPidUrnVctDisplay = { + locale: 'en', + name: 'PID (ARF, urn: vct)', + text_color: '#2F3544', + background_color: '#F1F2F0', + background_image: { + url: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +export const arfCompliantPidSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'arf-pid-sd-jwt', + vct: 'eu.europa.ec.eudi.pid.1', + display: [arfCompliantPidDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} satisfies SdJwtConfiguration + +export const arfCompliantPidUrnVctSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'arf-pid-sd-jwt-urn-vct', + vct: 'urn:eu.europa.ec.eudi:pid:1', + display: [arfCompliantPidUrnVctDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} satisfies SdJwtConfiguration + +const arfCompliantPidData = { + // Mandatory + family_name: 'Mustermann', + given_name: 'Erika', + birth_date: new DateOnly('1964-08-12'), + age_over_18: true, + nationality: 'DE', + + // Mandatory metadata + issuance_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), + issuing_country: 'DE', + issuing_authority: 'DE', + + // Optional: + age_over_12: true, + age_over_14: true, + age_over_16: true, + age_over_21: true, + age_over_65: false, + age_in_years: 40, + age_birth_year: 1984, + family_name_birth: 'GABLER', + birth_place: 'BERLIN', + resident_country: 'DE', + resident_city: 'KÖLN', + resident_postal_code: '51147', + resident_street: 'HEIDESTRASSE', + + // UC3 stuff + resident_address: 'HEIDESTRASSE 17, 51147, KÖLN', + resident_state: 'NORTH RHINE-WESTPHALIA', + resident_house_number: '17', + personal_administrative_number: '123123123123', + portrait: `data:image/jpeg;base64,${erikaPortrait.toString('base64')}`, + given_name_birth: 'Erika', + sex: 'female', + email_address: 'erika@mustermann.de', + mobile_phone_number: '+49 (0)30 901820', + document_number: '1119999', + issuing_jurisdiction: 'DE', + location_status: 'GOOD', +} + +export const arfCompliantPidSdJwtData = { + credentialConfigurationId: 'arf-pid-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + ...arfCompliantPidData, + vct: arfCompliantPidSdJwt.vct, + + issuance_date: arfCompliantPidData.issuance_date.toISOString(), + expiry_date: arfCompliantPidData.expiry_date.toISOString(), + + nbf: dateToSeconds(arfCompliantPidData.issuance_date), + exp: dateToSeconds(arfCompliantPidData.expiry_date), + }, + disclosureFrame: { + _sd: [ + // Mandatory + 'family_name', + 'given_name', + 'birth_date', + 'age_over_18', + + // Mandatory metadata + 'issuance_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + + // Optional + 'age_over_12', + 'age_over_14', + 'age_over_16', + 'age_over_21', + 'age_over_65', + 'age_in_years', + 'age_birth_year', + 'family_name_birth', + 'birth_place', + 'resident_country', + 'resident_city', + 'resident_postal_code', + 'resident_street', + 'nationality', + ], + }, + }, +} satisfies StaticSdJwtSignInput + +export const arfCompliantPidUrnVctSdJwtData = { + ...arfCompliantPidSdJwtData, + credentialConfigurationId: 'arf-pid-sd-jwt-urn-vct', + credential: { + ...arfCompliantPidSdJwtData.credential, + payload: { + ...arfCompliantPidSdJwtData.credential.payload, + vct: arfCompliantPidUrnVctSdJwt.vct, + }, + }, +} satisfies StaticSdJwtSignInput + +// https://animosolutions.getoutline.com/doc/certificate-of-residence-attestation-KjzG4n9VG0 +export const bdrIssuer = { + tags: [mobileDriversLicenseDisplay.name, 'ARF 1.5 PID (SD-JWT VC)'], + issuerId: '188e2459-6da8-4431-9062-2fcdac274f41', + credentialConfigurationsSupported: [ + { + 'vc+sd-jwt': { + configuration: mobileDriversLicenseSdJwt, + data: mobileDriversLicenseSdJwtData, + }, + mso_mdoc: { + configuration: mobileDriversLicenseMdoc, + data: mobileDriversLicenseMdocData, + }, + ldp_vc: { + configuration: mobileDriversLicenseLdpVc, + data: mobileDriversLicenseLdpVcData, + }, + }, + { + 'vc+sd-jwt': { + configuration: arfCompliantPidSdJwt, + data: arfCompliantPidSdJwtData, + }, + }, + { + 'vc+sd-jwt': { + configuration: arfCompliantPidUrnVctSdJwt, + data: arfCompliantPidUrnVctSdJwtData, + }, + }, + ] as const, + batchCredentialIssuance: { + batchSize: 10, + }, + display: [ + { + name: 'Bundesdruckerei', + logo: { + url: `${AGENT_HOST}/assets/issuers/bdr/issuer.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/issuer.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const bdrCredentialsData = { + [mobileDriversLicenseSdJwtData.credentialConfigurationId]: mobileDriversLicenseSdJwtData, + [mobileDriversLicenseMdocData.credentialConfigurationId]: mobileDriversLicenseMdocData, + [mobileDriversLicenseLdpVcData.credentialConfigurationId]: mobileDriversLicenseLdpVcData, + [arfCompliantPidSdJwtData.credentialConfigurationId]: arfCompliantPidSdJwtData, + [arfCompliantPidUrnVctSdJwtData.credentialConfigurationId]: arfCompliantPidUrnVctSdJwtData, +} diff --git a/agent/src/issuers/credentials/age18mDLMdoc.ts b/agent/src/issuers/credentials/age18mDLMdoc.ts new file mode 100644 index 0000000..983480f --- /dev/null +++ b/agent/src/issuers/credentials/age18mDLMdoc.ts @@ -0,0 +1,100 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { AGENT_HOST } from '../../constants' +import type { CredentialConfigurationDisplay, MdocConfiguration } from '../../issuer' +import type { StaticMdocSignInput } from '../../types' +import { oneYearInMilliseconds, serverStartupTimeInMilliseconds, tenDaysInMilliseconds } from '../../utils/date' +import { loadJPEGBufferSync } from '../../utils/image' + +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../../assets/erika.jpeg`) +const erikaSignature = loadJPEGBufferSync(`${__dirname}/../../../assets/signature.jpeg`) + +const mobileDriversLicenseDisplay = { + locale: 'en', + name: 'Driving Licence age=18', + text_color: '#6F5C77', + background_color: '#E6E2E7', + background_image: { + url: `${AGENT_HOST}/assets/verfiers/bdr/credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const mobileDriversLicensePayload = { + given_name: 'Tim', + family_name: 'Ooievaar', + birth_date: new DateOnly('2007-02-02'), + age_over_18: true, + nationality: 'DE', + document_number: 'Z021AB37X13', + portrait: new Uint8Array(erikaPortrait), + signature_usual_mark: new Uint8Array(erikaSignature), + resident_postal_code: '90210', + un_distinguishing_sign: 'D', + issuing_authority: 'Koningkrijk der Nederlanden', + issue_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), + // Must be same as C= in x509 cert (currently set to NL) + issuing_country: 'NL', + driving_privileges: [ + { + vehicle_category_code: 'B', + issue_date: new DateOnly('2004-01-15'), + expiry_date: new DateOnly('2009-01-14'), + codes: [ + { + code: 'B96', + value: '4250', + sign: '≤', + }, + { + code: '70', + value: '01.01', + }, + { + code: '95', + value: '2029-01-15', + }, + { + code: '96', + value: '750', + sign: '≤', + }, + ], + }, + ], +} + +export const age18mDLMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'mdl-mdoc', + doctype: 'org.iso.18013.5.1.mDL', + display: [mobileDriversLicenseDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const age18mDLMdocData = { + credentialConfigurationId: 'mdl-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: age18mDLMdoc.doctype, + namespaces: { + 'org.iso.18013.5.1': mobileDriversLicensePayload, + }, + validityInfo: { + validFrom: mobileDriversLicensePayload.issue_date, + validUntil: mobileDriversLicensePayload.expiry_date, + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: mobileDriversLicensePayload.issue_date, + }, + }, +} satisfies StaticMdocSignInput diff --git a/agent/src/issuers/credentials/ageSdJwt.ts b/agent/src/issuers/credentials/ageSdJwt.ts new file mode 100644 index 0000000..05bda91 --- /dev/null +++ b/agent/src/issuers/credentials/ageSdJwt.ts @@ -0,0 +1,56 @@ +import { ClaimFormat, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import type { SdJwtConfiguration } from '../../issuer' +import type { StaticSdJwtSignInput } from '../../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../../utils/date' + +const issuanceDate = new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds) +const expirationDate = new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds) + +export const ageSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'age-sd-jwt', + vct: 'urn:openid:interop:age:1', + display: [ + { + locale: 'en', + name: 'Age', + text_color: '#2F3544', + background_color: '#F1F2F0', + background_image: { + uri: '', + }, + }, + ], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} satisfies SdJwtConfiguration + +export const ageSdJwtData = { + credentialConfigurationId: 'age-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + vct: ageSdJwt.vct, + + age_over_18: true, + age_over_21: false, + + nbf: dateToSeconds(issuanceDate), + exp: dateToSeconds(expirationDate), + }, + disclosureFrame: { + _sd: ['age_over_18', 'age_over_21'], + }, + }, +} satisfies StaticSdJwtSignInput diff --git a/agent/src/issuers/credentials/arf18PidMdoc.ts b/agent/src/issuers/credentials/arf18PidMdoc.ts new file mode 100644 index 0000000..26cba7e --- /dev/null +++ b/agent/src/issuers/credentials/arf18PidMdoc.ts @@ -0,0 +1,86 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { AGENT_HOST } from '../../constants' +import type { MdocConfiguration } from '../../issuer' +import type { StaticMdocSignInput } from '../../types' +import { oneYearInMilliseconds, serverStartupTimeInMilliseconds } from '../../utils/date' +import { loadJPEGBufferSync } from '../../utils/image' + +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../../assets/erika.jpeg`) + +const arfPidPayload = { + family_name: 'Mustermann', + given_name: 'Erika', + birth_date: new DateOnly('1986-03-14'), + birth_place: 'The Netherlands, Leiden', + nationality: ['NL'], + resident_address: 'De Heyderweg 2, Leiden', + resident_country: 'NL', + resident_state: 'Zuid-Holland', + resident_city: 'Leiden', + resident_postal_code: '90210', + resident_street: 'De Heyderweg', + resident_house_number: '2', + personal_administrative_number: '9876543210', + portrait: new Uint8Array(erikaPortrait), + family_name_birth: 'Mustermann', + given_name_birth: 'Erika', + sex: 2, + email_address: 'erika@mustermann.nl', + mobile_phone_number: '+31717993005', + expiry_date: new DateOnly('2030-01-28'), + issuing_authority: 'Fime', + issuing_country: 'NL', + document_number: '0123456789', + issuance_date: new DateOnly(), + age_over_18: true, + age_in_years: 38, + age_birth_year: 1986, +} + +export const arfPidMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'government-arf-18-pid-mdoc', + doctype: 'eu.europa.ec.eudi.pid.1', + display: [ + { + locale: 'en', + name: 'ARF 1.8 PID', + text_color: '#2F3544', + background_color: '#F1F2F0', + background_image: { + url: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + }, + }, + ], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const arfPidMdocData = { + credentialConfigurationId: 'government-arf-18-pid-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: arfPidMdoc.doctype, + namespaces: { + [arfPidMdoc.doctype]: { + ...arfPidPayload, + }, + }, + validityInfo: { + validFrom: new Date(arfPidPayload.issuance_date.toISOString()), + validUntil: new Date(arfPidPayload.expiry_date.toISOString()), + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: new Date(arfPidPayload.issuance_date.toISOString()), + }, + }, +} satisfies StaticMdocSignInput diff --git a/agent/src/issuers/credentials/arf18PidSdJwt.ts b/agent/src/issuers/credentials/arf18PidSdJwt.ts new file mode 100644 index 0000000..72b16d7 --- /dev/null +++ b/agent/src/issuers/credentials/arf18PidSdJwt.ts @@ -0,0 +1,105 @@ +import { ClaimFormat, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { AGENT_HOST } from '../../constants' +import type { SdJwtConfiguration } from '../../issuer' +import type { StaticSdJwtSignInput } from '../../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../../utils/date' +import { loadJPEGBufferSync } from '../../utils/image' + +const issuanceDate = new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds) +const expirationDate = new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds) +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../../assets/erika.jpeg`) + +export const arfCompliantPidSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'government-arf-18-pid-sd-jwt', + vct: 'urn:eudi:pid:1', + display: [ + { + locale: 'en', + name: 'ARF 1.8 PID', + text_color: '#2F3544', + background_color: '#F1F2F0', + background_image: { + url: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + }, + }, + ], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} satisfies SdJwtConfiguration + +export const arfCompliantPidSdJwtData = { + credentialConfigurationId: 'government-arf-18-pid-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + vct: arfCompliantPidSdJwt.vct, + // Mandatory + family_name: 'Mustermann', + given_name: 'Erika', + birthdate: '1964-08-12', + + place_of_birth: { + country: 'NL', + region: 'Utrecht', + locality: 'Utrecht', + }, + nationalities: ['NL'], + + address: { + region: 'Utrecht', + locality: 'Utrecht', + country: 'NL', + postal_code: '90210', + }, + + // Mandatory metadata + issuing_country: 'NL', + issuing_authority: 'NL', + + // Extra + age_equal_or_over: { + 18: true, + }, + portrait: `data:image/jpeg;base64,${erikaPortrait.toString('base64')}`, + + issuance_date: issuanceDate.toISOString(), + expiry_date: expirationDate.toISOString(), + + nbf: dateToSeconds(issuanceDate), + exp: dateToSeconds(expirationDate), + }, + disclosureFrame: { + _sd: [ + // Mandatory + 'family_name', + 'given_name', + 'birthdate', + 'portrait', + 'issuance_date', + 'nationalities', + ], + place_of_birth: { + _sd: ['country', 'region', 'locality'], + }, + address: { + _sd: ['country', 'region', 'locality', 'postal_code'], + }, + age_equal_or_over: { + _sd: ['18'], + }, + }, + }, +} satisfies StaticSdJwtSignInput diff --git a/agent/src/issuers/credentials/arf18PidSdJwtNoNonDiscloseableFields.ts b/agent/src/issuers/credentials/arf18PidSdJwtNoNonDiscloseableFields.ts new file mode 100644 index 0000000..12287dc --- /dev/null +++ b/agent/src/issuers/credentials/arf18PidSdJwtNoNonDiscloseableFields.ts @@ -0,0 +1,111 @@ +import { ClaimFormat, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { AGENT_HOST } from '../../constants' +import type { SdJwtConfiguration } from '../../issuer' +import type { StaticSdJwtSignInput } from '../../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../../utils/date' +import { loadJPEGBufferSync } from '../../utils/image' + +const issuanceDate = new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds) +const expirationDate = new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds) +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../../assets/erika.jpeg`) + +export const arfCompliantPidSdJwtNoNonDisclosure = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'government-arf-18-pid-sd-jwt-no-non-disclosure', + vct: 'urn:eudi:pid:1', + display: [ + { + locale: 'en', + name: 'ARF 1.8 PID (No non-disclosure fields)', + text_color: '#2F3544', + background_color: '#F1F2F0', + background_image: { + url: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/pid-credential.png`, + }, + }, + ], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} satisfies SdJwtConfiguration + +export const arfCompliantPidSdJwtNoNonDisclosureData = { + credentialConfigurationId: 'government-arf-18-pid-sd-jwt-no-non-disclosure', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + vct: arfCompliantPidSdJwtNoNonDisclosure.vct, + // Mandatory + family_name: 'Mustermann', + given_name: 'Erika', + birthdate: '1964-08-12', + + place_of_birth: { + country: 'NL', + region: 'Utrecht', + locality: 'Utrecht', + }, + nationalities: ['NL'], + + address: { + region: 'Utrecht', + locality: 'Utrecht', + country: 'NL', + postal_code: '90210', + }, + + // Mandatory metadata + issuing_country: 'NL', + issuing_authority: 'NL', + + // Extra + age_equal_or_over: { + 18: true, + }, + portrait: `data:image/jpeg;base64,${erikaPortrait.toString('base64')}`, + + issuance_date: issuanceDate.toISOString(), + expiry_date: expirationDate.toISOString(), + + nbf: dateToSeconds(issuanceDate), + exp: dateToSeconds(expirationDate), + }, + disclosureFrame: { + _sd: [ + // Mandatory + 'family_name', + 'given_name', + 'birthdate', + 'portrait', + 'issuance_date', + 'expiry_date', + 'nationalities', + 'place_of_birth', + 'address', + 'issuing_country', + 'issuing_authority', + 'age_equal_or_over', + ], + place_of_birth: { + _sd: ['country', 'region', 'locality'], + }, + address: { + _sd: ['country', 'region', 'locality', 'postal_code'], + }, + age_equal_or_over: { + _sd: ['18'], + }, + }, + }, +} satisfies StaticSdJwtSignInput diff --git a/agent/src/issuers/credentials/mDLMdoc.ts b/agent/src/issuers/credentials/mDLMdoc.ts new file mode 100644 index 0000000..a46b825 --- /dev/null +++ b/agent/src/issuers/credentials/mDLMdoc.ts @@ -0,0 +1,99 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { AGENT_HOST } from '../../constants' +import type { CredentialConfigurationDisplay, MdocConfiguration } from '../../issuer' +import type { StaticMdocSignInput } from '../../types' +import { oneYearInMilliseconds, serverStartupTimeInMilliseconds, tenDaysInMilliseconds } from '../../utils/date' +import { loadJPEGBufferSync } from '../../utils/image' + +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../../assets/erika.jpeg`) +const erikaSignature = loadJPEGBufferSync(`${__dirname}/../../../assets/signature.jpeg`) + +const mobileDriversLicenseDisplay = { + locale: 'en', + name: 'Driving Licence', + text_color: '#6F5C77', + background_color: '#E6E2E7', + background_image: { + url: `${AGENT_HOST}/assets/verfiers/bdr/credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const mobileDriversLicensePayload = { + given_name: 'Erika', + family_name: 'Mustermann', + birth_date: new DateOnly('1964-08-12'), + age_over_18: true, + document_number: 'Z021AB37X13', + portrait: new Uint8Array(erikaPortrait), + signature_usual_mark: new Uint8Array(erikaSignature), + resident_postal_code: '90210', + un_distinguishing_sign: 'D', + issuing_authority: 'Bundesrepublik Deutschland', + issue_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), + // Must be same as C= in x509 cert (currently set to NL) + issuing_country: 'NL', + driving_privileges: [ + { + vehicle_category_code: 'B', + issue_date: new DateOnly('2024-01-15'), + expiry_date: new DateOnly('2039-01-14'), + codes: [ + { + code: 'B96', + value: '4250', + sign: '≤', + }, + { + code: '70', + value: '01.01', + }, + { + code: '95', + value: '2029-01-15', + }, + { + code: '96', + value: '750', + sign: '≤', + }, + ], + }, + ], +} + +export const mobileDriversLicenseMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'mdl-mdoc', + doctype: 'org.iso.18013.5.1.mDL', + display: [mobileDriversLicenseDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const mobileDriversLicenseMdocData = { + credentialConfigurationId: 'mdl-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: mobileDriversLicenseMdoc.doctype, + namespaces: { + 'org.iso.18013.5.1': mobileDriversLicensePayload, + }, + validityInfo: { + validFrom: mobileDriversLicensePayload.issue_date, + validUntil: mobileDriversLicensePayload.expiry_date, + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: mobileDriversLicensePayload.issue_date, + }, + }, +} satisfies StaticMdocSignInput diff --git a/agent/src/issuers/credentials/openIDSdJwt.ts b/agent/src/issuers/credentials/openIDSdJwt.ts new file mode 100644 index 0000000..e57995b --- /dev/null +++ b/agent/src/issuers/credentials/openIDSdJwt.ts @@ -0,0 +1,57 @@ +import { ClaimFormat, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import type { SdJwtConfiguration } from '../../issuer' +import type { StaticSdJwtSignInput } from '../../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../../utils/date' + +const issuanceDate = new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds) +const expirationDate = new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds) + +export const openIdSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'openid-id-sd-jwt', + vct: 'urn:openid:interop:id:1', + display: [ + { + locale: 'en', + name: 'OpenID ID', + text_color: '#2F3544', + background_color: '#F1F2F0', + background_image: { + uri: '', + }, + }, + ], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} satisfies SdJwtConfiguration + +export const openIdSdJwtData = { + credentialConfigurationId: 'openid-id-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + vct: openIdSdJwt.vct, + + openid: '3b158a23-82d2-4933-80b0-1d3b13a1461e', + family_name: 'Mustermann', + given_name: 'Erika', + + nbf: dateToSeconds(issuanceDate), + exp: dateToSeconds(expirationDate), + }, + disclosureFrame: { + _sd: ['family_name', 'given_name', 'openid'], + }, + }, +} satisfies StaticSdJwtSignInput diff --git a/agent/src/issuers/credentials/over70mDLMdoc.ts b/agent/src/issuers/credentials/over70mDLMdoc.ts new file mode 100644 index 0000000..40daee5 --- /dev/null +++ b/agent/src/issuers/credentials/over70mDLMdoc.ts @@ -0,0 +1,99 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { AGENT_HOST } from '../../constants' +import type { CredentialConfigurationDisplay, MdocConfiguration } from '../../issuer' +import type { StaticMdocSignInput } from '../../types' +import { oneYearInMilliseconds, serverStartupTimeInMilliseconds, tenDaysInMilliseconds } from '../../utils/date' +import { loadJPEGBufferSync } from '../../utils/image' + +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../../assets/erika.jpeg`) +const erikaSignature = loadJPEGBufferSync(`${__dirname}/../../../assets/signature.jpeg`) + +const mobileDriversLicenseDisplay = { + locale: 'en', + name: 'Driving Licence age>70', + text_color: '#6F5C77', + background_color: '#E6E2E7', + background_image: { + url: `${AGENT_HOST}/assets/verfiers/bdr/credential.png`, + uri: `${AGENT_HOST}/assets/issuers/bdr/credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const mobileDriversLicensePayload = { + given_name: 'Cora', + family_name: 'de Oever Rijshoven', + birth_date: new DateOnly('1954-02-02'), + age_over_18: true, + document_number: 'Z021AB37X13', + portrait: new Uint8Array(erikaPortrait), + signature_usual_mark: new Uint8Array(erikaSignature), + resident_postal_code: '90210', + un_distinguishing_sign: 'D', + issuing_authority: 'Bundesrepublik Deutschland', + issue_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), + // Must be same as C= in x509 cert (currently set to NL) + issuing_country: 'NL', + driving_privileges: [ + { + vehicle_category_code: 'B', + issue_date: new DateOnly('2004-01-15'), + expiry_date: new DateOnly('2009-01-14'), + codes: [ + { + code: 'B96', + value: '4250', + sign: '≤', + }, + { + code: '70', + value: '01.01', + }, + { + code: '95', + value: '2029-01-15', + }, + { + code: '96', + value: '750', + sign: '≤', + }, + ], + }, + ], +} + +export const over70mDLMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'mdl-mdoc', + doctype: 'org.iso.18013.5.1.mDL', + display: [mobileDriversLicenseDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const over70mDLMdocData = { + credentialConfigurationId: 'mdl-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: over70mDLMdoc.doctype, + namespaces: { + 'org.iso.18013.5.1': mobileDriversLicensePayload, + }, + validityInfo: { + validFrom: mobileDriversLicensePayload.issue_date, + validUntil: mobileDriversLicensePayload.expiry_date, + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: mobileDriversLicensePayload.issue_date, + }, + }, +} satisfies StaticMdocSignInput diff --git a/agent/src/issuers/credentials/photoIdMdoc.ts b/agent/src/issuers/credentials/photoIdMdoc.ts new file mode 100644 index 0000000..1d681dd --- /dev/null +++ b/agent/src/issuers/credentials/photoIdMdoc.ts @@ -0,0 +1,104 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import type { CredentialConfigurationDisplay, MdocConfiguration } from '../../issuer' +import type { StaticMdocSignInput } from '../../types' +import { oneYearInMilliseconds, serverStartupTimeInMilliseconds } from '../../utils/date' +import { loadJPEGBufferSync } from '../../utils/image' + +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../../assets/erika.jpeg`) + +const photoIdDisplay = { + locale: 'en', + name: 'Photo ID', + text_color: '#525C75', + background_color: '#F5F7F8', + background_image: { + uri: '', + }, +} satisfies CredentialConfigurationDisplay + +const photoIdPayload = { + family_name_unicode: 'Mustermann', + family_name_latin1: 'Mustermann', + given_name_unicode: 'Erika', + given_name_latin1: 'Erika', + birth_date: new DateOnly('1986-03-14'), + portrait: new Uint8Array(erikaPortrait), + issue_date: new DateOnly(), + expiry_date: new DateOnly('2029-08-01'), + issuing_authority_unicode: 'Fime', + issuing_country: 'NL', + age_over_18: true, + age_in_years: 38, + age_birth_year: 1986, + portrait_capture_date: new Date('2022-11-14T00:00:00Z'), + birthplace: 'The Netherlands, Leiden', + name_at_birth: 'Erika Mustermann', + resident_address_unicode: 'De Heyderweg 2, Leiden', + resident_city_unicode: 'Leiden', + resident_postal_code: '90210', + resident_country: 'NL', + sex: 2, + nationality: 'NL', + document_number: '0123456789', +} + +const photoIdPayload_2 = { + person_id: '1234567890', + birth_country: 'NL', + birth_state: 'Zuid-Holland', + birth_city: 'Leiden', + administrative_number: '9876543210', + resident_street: 'De Heyderweg', + resident_house_number: '2', + travel_document_number: 'C11T002JM', +} + +// FIXME: Document has insane large values for these fields +const photoIdPayload_3 = { + dg1: Uint8Array.from(Buffer.from('many bytes')), + dg2: Uint8Array.from(Buffer.from('many bytes')), + sod: Uint8Array.from(Buffer.from('many bytes')), +} + +export const photoIdMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'government-photo-id-mdoc', + doctype: 'org.iso.23220.photoID.1', + display: [photoIdDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const photoIdMdocData = { + credentialConfigurationId: 'government-photo-id-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: photoIdMdoc.doctype, + namespaces: { + 'org.iso.23220.1': { + ...photoIdPayload, + }, + 'org.iso.23220.photoID.1': { + ...photoIdPayload_2, + }, + 'org.iso.23220.datagroups.1': { + ...photoIdPayload_3, + }, + }, + validityInfo: { + validFrom: new Date(photoIdPayload.issue_date.toISOString()), + validUntil: new Date(photoIdPayload.expiry_date.toISOString()), + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: new Date(photoIdPayload.issue_date.toISOString()), + }, + }, +} satisfies StaticMdocSignInput diff --git a/agent/src/issuers/index.ts b/agent/src/issuers/index.ts new file mode 100644 index 0000000..636d105 --- /dev/null +++ b/agent/src/issuers/index.ts @@ -0,0 +1,36 @@ +import type { PlaygroundIssuerOptions } from '../issuer' +import { bdrCredentialsData, bdrIssuer } from './bdr' +import { kolnCredentialsData, kolnIssuer } from './koln' +import { krankenkasseCredentialsData, krankenkasseIssuer } from './krankenkasse' +import { mvrcCredentialsData, mvrcIssuer } from './mvrc' +import { nederlandenCredentialsData } from './nederlanden' +import { nederlandenIssuer } from './nederlanden' +import { openIdInteropData, openIdInteropEventGovernmentIssuer } from './openIdInteropEvent' +import { steuernCredentialsData, steuernIssuer } from './steuern' +import { telOrgCredentialsData, telOrgIssuer } from './telOrg' +import { vwsCredentialsData } from './vws' +import { vwsIssuer } from './vws' + +export const issuers: PlaygroundIssuerOptions[] = [ + bdrIssuer, + kolnIssuer, + steuernIssuer, + krankenkasseIssuer, + telOrgIssuer, + nederlandenIssuer, + mvrcIssuer, + vwsIssuer, + openIdInteropEventGovernmentIssuer, +] + +export const issuersCredentialsData = { + ...bdrCredentialsData, + ...kolnCredentialsData, + ...steuernCredentialsData, + ...krankenkasseCredentialsData, + ...telOrgCredentialsData, + ...nederlandenCredentialsData, + ...mvrcCredentialsData, + ...vwsCredentialsData, + ...openIdInteropData, +} diff --git a/agent/src/issuers/koln.ts b/agent/src/issuers/koln.ts new file mode 100644 index 0000000..3cdabe0 --- /dev/null +++ b/agent/src/issuers/koln.ts @@ -0,0 +1,149 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' + +import { AGENT_HOST } from '../constants' +import type { + CredentialConfigurationDisplay, + MdocConfiguration, + PlaygroundIssuerOptions, + SdJwtConfiguration, +} from '../issuer' +import type { StaticMdocSignInput, StaticSdJwtSignInput } from '../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../utils/date' + +const certificateOfResidenceDisplay = { + locale: 'en', + name: 'Certificate of Residence', + text_color: '#525C75', + background_color: '#E4DEDD', + background_image: { + url: `${AGENT_HOST}/assets/issuers/koln/credential.png`, + uri: `${AGENT_HOST}/assets/issuers/koln/credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const certificateOfResidencePayload = { + family_name: 'Mustermann', + given_name: 'Erika', + birth_date: new DateOnly('1964-08-12'), + resident_address: 'Heidestrasse 17, 51147 Koln', + gender: 2, + birth_place: 'Köln', + arrival_date: new DateOnly('2024-03-01'), + nationality: 'DE', + issuance_date: new DateOnly(new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds).toISOString()), + expiry_date: new DateOnly(new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds).toISOString()), + issuing_country: 'DE', +} + +export const certificateOfResidenceMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'certificate-of-residence-mdoc', + doctype: 'eu.europa.ec.eudi.cor.1', + display: [certificateOfResidenceDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const certificateOfResidenceMdocData = { + credentialConfigurationId: 'certificate-of-residence-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: certificateOfResidenceMdoc.doctype, + namespaces: { + [certificateOfResidenceMdoc.doctype]: { ...certificateOfResidencePayload }, + }, + validityInfo: { + validFrom: new Date(certificateOfResidencePayload.issuance_date.toISOString()), + validUntil: new Date(certificateOfResidencePayload.expiry_date.toISOString()), + }, + }, +} satisfies StaticMdocSignInput + +export const certificateOfResidenceSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'certificate-of-residence-sd-jwt', + vct: 'https://example.eudi.ec.europa.eu/cor/1', + display: [certificateOfResidenceDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies SdJwtConfiguration + +export const certificateOfResidenceSdJwtData = { + credentialConfigurationId: 'certificate-of-residence-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + ...certificateOfResidencePayload, + birth_date: certificateOfResidencePayload.birth_date.toISOString(), + arrival_date: certificateOfResidencePayload.arrival_date.toISOString(), + nbf: dateToSeconds(certificateOfResidencePayload.issuance_date), + exp: dateToSeconds(certificateOfResidencePayload.expiry_date), + issuance_date: certificateOfResidencePayload.issuance_date.toISOString(), + expiry_date: certificateOfResidencePayload.expiry_date.toISOString(), + vct: certificateOfResidenceSdJwt.vct, + }, + disclosureFrame: { + _sd: [ + 'family_name', + 'given_name', + 'resident_address', + 'birth_date', + 'gender', + 'birth_place', + 'arrival_date', + 'nationality', + ], + }, + }, +} satisfies StaticSdJwtSignInput + +// https://animosolutions.getoutline.com/doc/certificate-of-residence-attestation-KjzG4n9VG0 +export const kolnIssuer = { + tags: [certificateOfResidenceDisplay.name], + issuerId: '832f1c72-817d-4a54-b0fc-9994ecaba291', + credentialConfigurationsSupported: [ + { + 'vc+sd-jwt': { + configuration: certificateOfResidenceSdJwt, + data: certificateOfResidenceSdJwtData, + }, + mso_mdoc: { + configuration: certificateOfResidenceMdoc, + data: certificateOfResidenceMdocData, + }, + }, + ], + batchCredentialIssuance: { + batchSize: 10, + }, + display: [ + { + name: 'Bürgeramt Köln', + logo: { + url: `${AGENT_HOST}/assets/issuers/koln/issuer.png`, + uri: `${AGENT_HOST}/assets/issuers/koln/issuer.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const kolnCredentialsData = { + [certificateOfResidenceSdJwtData.credentialConfigurationId]: certificateOfResidenceSdJwtData, + [certificateOfResidenceMdocData.credentialConfigurationId]: certificateOfResidenceMdocData, +} diff --git a/agent/src/issuers/krankenkasse.ts b/agent/src/issuers/krankenkasse.ts new file mode 100644 index 0000000..95c856d --- /dev/null +++ b/agent/src/issuers/krankenkasse.ts @@ -0,0 +1,134 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { AGENT_HOST } from '../constants' +import type { + CredentialConfigurationDisplay, + MdocConfiguration, + PlaygroundIssuerOptions, + SdJwtConfiguration, +} from '../issuer' +import type { StaticMdocSignInput, StaticSdJwtSignInput } from '../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../utils/date' + +const healthIdDisplay = { + locale: 'en', + name: 'Health-ID', + text_color: '#FFFFFF', + background_color: '#61719D', + background_image: { + url: `${AGENT_HOST}/assets/issuers/krankenkasse/credential.png`, + uri: `${AGENT_HOST}/assets/issuers/krankenkasse/credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const healthIdPayload = { + health_insurance_id: 'A123456780101575519DE', + affiliation_country: 'DE', + wallet_e_prescription_code: + '160.000.033.491.352.56&94c75e15e4c4dd6b50e3c18b92b4754e88fec4ab144e86a1b95df1209767978b&medication name', + issuance_date: new DateOnly(new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds).toISOString()), + expiry_date: new DateOnly(new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds).toISOString()), + issuing_authority: 'DE', + issuing_country: 'DE', +} + +export const healthIdMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'health-id-mdoc', + doctype: 'eu.europa.ec.eudi.hiid.1', + display: [healthIdDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const healthIdMdocData = { + credentialConfigurationId: 'health-id-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: healthIdMdoc.doctype, + namespaces: { + [healthIdMdoc.doctype]: healthIdPayload, + }, + validityInfo: { + validFrom: new Date(healthIdPayload.issuance_date.toISOString()), + validUntil: new Date(healthIdPayload.expiry_date.toISOString()), + }, + }, +} satisfies StaticMdocSignInput + +export const healthIdSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'health-id-sd-jwt', + vct: 'eu.europa.ec.eudi.hiid.1', + display: [healthIdDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies SdJwtConfiguration + +export const healthIdSdJwtData = { + credentialConfigurationId: 'health-id-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + ...healthIdPayload, + nbf: dateToSeconds(healthIdPayload.issuance_date), + exp: dateToSeconds(healthIdPayload.expiry_date), + issuance_date: healthIdPayload.issuance_date.toISOString(), + expiry_date: healthIdPayload.expiry_date.toISOString(), + vct: healthIdSdJwt.vct, + }, + disclosureFrame: { + _sd: ['health_insurance_id', 'affiliation_country', 'wallet_e_prescription_code'], + }, + }, +} satisfies StaticSdJwtSignInput + +// https://animosolutions.getoutline.com/doc/certificate-of-residence-attestation-KjzG4n9VG0 +export const krankenkasseIssuer = { + tags: [healthIdDisplay.name], + issuerId: 'a27a9f50-2b4d-4fac-99b6-9fd306641f9d', + credentialConfigurationsSupported: [ + { + 'vc+sd-jwt': { + configuration: healthIdSdJwt, + data: healthIdSdJwtData, + }, + mso_mdoc: { + configuration: healthIdMdoc, + data: healthIdMdocData, + }, + }, + ], + batchCredentialIssuance: { + batchSize: 10, + }, + display: [ + { + name: 'Die Krankenkasse', + logo: { + url: `${AGENT_HOST}/assets/issuers/krankenkasse/issuer.png`, + uri: `${AGENT_HOST}/assets/issuers/krankenkasse/issuer.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const krankenkasseCredentialsData = { + [healthIdSdJwtData.credentialConfigurationId]: healthIdSdJwtData, + [healthIdMdocData.credentialConfigurationId]: healthIdMdocData, +} diff --git a/agent/src/issuers/mvrc.ts b/agent/src/issuers/mvrc.ts new file mode 100644 index 0000000..f5a72fa --- /dev/null +++ b/agent/src/issuers/mvrc.ts @@ -0,0 +1,147 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' + +import { AGENT_HOST } from '../constants' +import type { CredentialConfigurationDisplay, MdocConfiguration, PlaygroundIssuerOptions } from '../issuer' +import type { StaticMdocSignInput } from '../types' +import { oneYearInMilliseconds, serverStartupTimeInMilliseconds } from '../utils/date' + +const mvrcDisplay = { + locale: 'en', + name: 'Vehicle Registration', + text_color: '#FFFFFF', + background_color: '#CD3401', + background_image: { + url: '', + uri: '', + }, +} satisfies CredentialConfigurationDisplay + +const mvrcPayload = { + issue_date: new DateOnly('2025-01-28'), + expiry_date: new DateOnly('2034-08-28'), + issuing_country: 'NL', + issuing_authority_unicode: 'Fime', + document_number: '0123456789', +} + +const mvrcPayload_2 = { + issue_date: new DateOnly('2025-01-28'), + expiry_date: new DateOnly('2034-08-28'), + issuing_country: 'NL', + issuing_authority_unicode: 'Fime', + document_number: '0123456789', + registration_number: '11MM05', + date_of_registration: new Date('2021-12-20T17:45:00Z'), + date_of_first_registration: '2020-07-14', + vehicle_identification_number: 'PD02-5016890', + vehicle_holder: [ + { + family_name_unicode: 'baron Van der Cërnosljé', + family_name_latin1: 'baron Van der Cërnosljé', + given_name_unicode: 'CBA', + given_name_latin1: 'CBA', + // resident_address: '', + // resident_city: '', + // resident_country: '', + }, + ], + basic_vehicle_info: { + vehicle_category_code: 'M1', + type_approval_number: 'e1-test', + make: 'OPEL', + commercial_name: 'MITSU', + colours: [4, 9], + }, + mass_info: { + unit: 'kg', + techn_perm_max_laden_mass: 1290, + vehicle_max_mass: 1150, + whole_vehicle_max_mass: 2500, + mass_in_running_order: 920, + }, + trailer_mass_info: { + unit: 'kg', + tech_perm_max_tow_mass_braked_trail: 1750, + tech_perm_max_tow_mass_unbr_trailer: 459, + }, + engine_info: { + engine_capacity: 999, + engine_power: 52, + energy_source: [15], + }, + seating_info: { + nr_of_seating_positions: 5, + number_of_standing_places: 1, + }, + un_distinguishing_sign: 'NLD', +} + +export const mvrcMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'mvrc-mdoc', + doctype: 'org.iso.7367.1.mVRC', + display: [mvrcDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const mvrcMdocData = { + credentialConfigurationId: 'mvrc-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: mvrcMdoc.doctype, + namespaces: { + 'org.iso.23220.1': { + ...mvrcPayload, + }, + 'org.iso.7367.1': { + ...mvrcPayload_2, + }, + }, + validityInfo: { + validFrom: new Date(mvrcPayload.issue_date.toISOString()), + validUntil: new Date(mvrcPayload.expiry_date.toISOString()), + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: new Date(mvrcPayload.issue_date.toISOString()), + }, + }, +} satisfies StaticMdocSignInput + +// https://animosolutions.getoutline.com/doc/credential-msisdn-1BljW1GEM0 +export const mvrcIssuer = { + tags: [mvrcDisplay.name], + issuerId: '2943c018-8eac-4ddc-a234-da35857fa3e9', + credentialConfigurationsSupported: [ + { + mso_mdoc: { + configuration: mvrcMdoc, + data: mvrcMdocData, + }, + }, + ], + batchCredentialIssuance: { + batchSize: 10, + }, + display: [ + { + name: 'RDW', + logo: { + url: `${AGENT_HOST}/assets/issuers/mvrc/issuer.png`, + uri: `${AGENT_HOST}/assets/issuers/mvrc/issuer.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const mvrcCredentialsData = { + [mvrcMdocData.credentialConfigurationId]: mvrcMdocData, +} diff --git a/agent/src/issuers/nederlanden.ts b/agent/src/issuers/nederlanden.ts new file mode 100644 index 0000000..a69ccac --- /dev/null +++ b/agent/src/issuers/nederlanden.ts @@ -0,0 +1,299 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' + +import { AGENT_HOST } from '../constants' +import type { + CredentialConfigurationDisplay, + MdocConfiguration, + PlaygroundIssuerOptions, + SdJwtConfiguration, +} from '../issuer' +import type { StaticMdocSignInput, StaticSdJwtSignInput } from '../types' +import { dateToSeconds, oneYearInMilliseconds, serverStartupTimeInMilliseconds } from '../utils/date' + +import { loadJPEGBufferSync } from '../utils/image' + +// Reuse from BDR, not exactly the same as in document though +const erikaPortrait = loadJPEGBufferSync(`${__dirname}/../../assets/erika.jpeg`) + +const photoIdDisplay = { + locale: 'en', + name: 'Photo ID', + text_color: '#525C75', + background_color: '#F5F7F8', + background_image: { + url: '', + uri: '', + }, +} satisfies CredentialConfigurationDisplay + +const photoIdPayload = { + family_name_unicode: 'Mustermann', + family_name_latin1: 'Mustermann', + given_name_unicode: 'Erika', + given_name_latin1: 'Erika', + birth_date: new DateOnly('1986-03-14'), + portrait: new Uint8Array(erikaPortrait), + issue_date: new DateOnly('2024-08-01'), + expiry_date: new DateOnly('2029-08-01'), + issuing_authority_unicode: 'Fime', + issuing_country: 'NL', + age_over_18: true, + age_in_years: 38, + age_birth_year: 1986, + portrait_capture_date: new Date('2022-11-14T00:00:00Z'), + birthplace: 'The Netherlands, Leiden', + name_at_birth: 'Erika Mustermann', + resident_address_unicode: 'De Heyderweg 2, Leiden', + resident_city_unicode: 'Leiden', + resident_postal_code: '2314 XZ', + resident_country: 'NL', + sex: 2, + nationality: 'NL', + document_number: '0123456789', +} + +const photoIdPayload_2 = { + person_id: '1234567890', + birth_country: 'NL', + birth_state: 'Zuid-Holland', + birth_city: 'Leiden', + administrative_number: '9876543210', + resident_street: 'De Heyderweg', + resident_house_number: '2', + travel_document_number: 'C11T002JM', +} + +// FIXME: Document has insane large values for these fields +const photoIdPayload_3 = { + dg1: 'many bytes', + dg2: 'many bytes', + sod: 'many bytes', + // dg1: Uint8Array.from(Buffer.from('', 'hex')), + // dg2: Uint8Array.from(Buffer.from('', 'hex')), + // sod: Uint8Array.from(Buffer.from('', 'hex')), +} + +export const photoIdMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'photo-id-mdoc', + doctype: 'org.iso.23220.photoID.1', + display: [photoIdDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const photoIdMdocData = { + credentialConfigurationId: 'photo-id-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: photoIdMdoc.doctype, + namespaces: { + 'org.iso.23220.1': { + ...photoIdPayload, + }, + 'org.iso.23220.photoID.1': { + ...photoIdPayload_2, + }, + 'org.iso.23220.datagroups.1': { + ...photoIdPayload_3, + }, + }, + validityInfo: { + validFrom: new Date(photoIdPayload.issue_date.toISOString()), + validUntil: new Date(photoIdPayload.expiry_date.toISOString()), + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: new Date(photoIdPayload.issue_date.toISOString()), + }, + }, +} satisfies StaticMdocSignInput + +// ======= EU PID 1.5 ======= + +const eudiPidDisplay = { + locale: 'en', + name: 'EU PID 1.5', + text_color: '#525C75', + background_color: '#EBF1F3', + background_image: { + url: '', + uri: '', + }, +} satisfies CredentialConfigurationDisplay + +const eudiPidPayload = { + family_name: 'Mustermann', + given_name: 'Erika', + birth_date: new DateOnly('1986-03-14'), + birth_place: 'The Netherlands, Leiden', + nationality: ['NL'], + resident_address: 'De Heyderweg 2, Leiden', + resident_country: 'NL', + resident_state: 'Zuid-Holland', + resident_city: 'Leiden', + resident_postal_code: '2314 XZ', + resident_street: 'De Heyderweg', + resident_house_number: '2', + personal_administrative_number: '9876543210', + portrait: new Uint8Array(erikaPortrait), + family_name_birth: 'Mustermann', + given_name_birth: 'Erika', + sex: 2, + email_address: 'erika@mustermann.nl', + mobile_phone_number: '+31717993005', + expiry_date: new DateOnly('2030-01-28'), + issuing_authority: 'Fime', + issuing_country: 'NL', + document_number: '0123456789', + issuance_date: new DateOnly('2025-01-28'), + age_over_18: true, + age_in_years: 38, + age_birth_year: 1986, +} + +export const eudiPidMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'eudi-pid-mdoc', + doctype: 'eu.europa.ec.eudi.pid.1', + display: [eudiPidDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const eudiPidMdocData = { + credentialConfigurationId: 'eudi-pid-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: eudiPidMdoc.doctype, + namespaces: { + [eudiPidMdoc.doctype]: { + ...eudiPidPayload, + }, + }, + validityInfo: { + validFrom: new Date(eudiPidPayload.issuance_date.toISOString()), + validUntil: new Date(eudiPidPayload.expiry_date.toISOString()), + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: new Date(eudiPidPayload.issuance_date.toISOString()), + }, + }, +} satisfies StaticMdocSignInput + +export const eudiPidSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'eudi-pid-sd-jwt', + vct: 'eu.europa.ec.eudi.pid.1', + display: [eudiPidDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies SdJwtConfiguration + +export const eudiPidSdJwtData = { + credentialConfigurationId: 'eudi-pid-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + ...eudiPidPayload, + nbf: dateToSeconds(eudiPidPayload.issuance_date), + exp: dateToSeconds(eudiPidPayload.expiry_date), + issuance_date: eudiPidPayload.issuance_date.toISOString(), + expiry_date: eudiPidPayload.expiry_date.toISOString(), + vct: eudiPidSdJwt.vct, + portrait: `data:image/jpeg;base64,${erikaPortrait.toString('base64')}`, + }, + disclosureFrame: { + _sd: [ + 'family_name', + 'given_name', + 'birth_date', + 'birth_place', + 'nationality', + 'resident_address', + 'resident_country', + 'resident_state', + 'resident_city', + 'resident_postal_code', + 'resident_street', + 'resident_house_number', + 'personal_administrative_number', + 'portrait', + 'family_name_birth', + 'given_name_birth', + 'sex', + 'email_address', + 'mobile_phone_number', + 'expiry_date', + 'issuing_authority', + 'issuing_country', + 'document_number', + 'issuance_date', + 'age_over_18', + 'age_in_years', + 'age_birth_year', + ], + }, + }, +} satisfies StaticSdJwtSignInput + +// https://animosolutions.getoutline.com/doc/credential-msisdn-1BljW1GEM0 +export const nederlandenIssuer = { + tags: [photoIdDisplay.name, 'ARF 1.5 PID'], + issuerId: '40adc717-933f-471c-ae42-0f5e92b3cca1', + credentialConfigurationsSupported: [ + { + 'vc+sd-jwt': { + configuration: eudiPidSdJwt, + data: eudiPidSdJwtData, + }, + mso_mdoc: { + configuration: eudiPidMdoc, + data: eudiPidMdocData, + }, + }, + { + mso_mdoc: { + configuration: photoIdMdoc, + data: photoIdMdocData, + }, + }, + ], + batchCredentialIssuance: { + batchSize: 10, + }, + display: [ + { + name: 'Koninkrijk der Nederlanden', + logo: { + url: `${AGENT_HOST}/assets/issuers/nederlanden/issuer.png`, + uri: `${AGENT_HOST}/assets/issuers/nederlanden/issuer.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const nederlandenCredentialsData = { + [photoIdMdocData.credentialConfigurationId]: photoIdMdocData, + [eudiPidSdJwtData.credentialConfigurationId]: eudiPidSdJwtData, + [eudiPidMdocData.credentialConfigurationId]: eudiPidMdocData, +} diff --git a/agent/src/issuers/openIdInteropEvent.ts b/agent/src/issuers/openIdInteropEvent.ts new file mode 100644 index 0000000..7281cae --- /dev/null +++ b/agent/src/issuers/openIdInteropEvent.ts @@ -0,0 +1,82 @@ +import { AGENT_HOST } from '../constants' +import type { PlaygroundIssuerOptions } from '../issuer' +import { ageSdJwt, ageSdJwtData } from './credentials/ageSdJwt' +import { arfPidMdoc, arfPidMdocData } from './credentials/arf18PidMdoc' +import { arfCompliantPidSdJwt, arfCompliantPidSdJwtData } from './credentials/arf18PidSdJwt' +import { + arfCompliantPidSdJwtNoNonDisclosure, + arfCompliantPidSdJwtNoNonDisclosureData, +} from './credentials/arf18PidSdJwtNoNonDiscloseableFields' +import { mobileDriversLicenseMdoc, mobileDriversLicenseMdocData } from './credentials/mDLMdoc' +import { openIdSdJwt, openIdSdJwtData } from './credentials/openIDSdJwt' +import { photoIdMdoc, photoIdMdocData } from './credentials/photoIdMdoc' + +export const openIdInteropEventGovernmentIssuer = { + tags: ['mDL', 'ARF 1.8 PID', 'Photo ID', 'OpenID', 'Age'], + issuerId: 'b39e71cf-9cf1-4723-a9cd-66f42a510b36', + credentialConfigurationsSupported: [ + { + mso_mdoc: { + configuration: mobileDriversLicenseMdoc, + data: mobileDriversLicenseMdocData, + }, + }, + { + 'vc+sd-jwt': { + configuration: arfCompliantPidSdJwt, + data: arfCompliantPidSdJwtData, + }, + mso_mdoc: { + configuration: arfPidMdoc, + data: arfPidMdocData, + }, + }, + { + 'vc+sd-jwt': { + configuration: arfCompliantPidSdJwtNoNonDisclosure, + data: arfCompliantPidSdJwtNoNonDisclosureData, + }, + }, + { + mso_mdoc: { + configuration: photoIdMdoc, + data: photoIdMdocData, + }, + }, + { + 'vc+sd-jwt': { + configuration: openIdSdJwt, + data: openIdSdJwtData, + }, + }, + { + 'vc+sd-jwt': { + configuration: ageSdJwt, + data: ageSdJwtData, + }, + }, + ] as const, + batchCredentialIssuance: { + batchSize: 10, + }, + playgroundDisplayName: 'Utopia Government (OpenID Interop Event)', + display: [ + { + name: 'Utopia Government', + logo: { + url: `${AGENT_HOST}/assets/verifiers/government.png`, + uri: `${AGENT_HOST}/assets/verifiers/government.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const openIdInteropData = { + [mobileDriversLicenseMdocData.credentialConfigurationId]: mobileDriversLicenseMdocData, + [arfCompliantPidSdJwtData.credentialConfigurationId]: arfCompliantPidSdJwtData, + [arfPidMdocData.credentialConfigurationId]: arfPidMdocData, + [photoIdMdocData.credentialConfigurationId]: photoIdMdocData, + [openIdSdJwtData.credentialConfigurationId]: openIdSdJwtData, + [ageSdJwtData.credentialConfigurationId]: ageSdJwtData, + [arfCompliantPidSdJwtNoNonDisclosureData.credentialConfigurationId]: arfCompliantPidSdJwtNoNonDisclosureData, +} diff --git a/agent/src/issuers/steuern.ts b/agent/src/issuers/steuern.ts new file mode 100644 index 0000000..4a1c9f4 --- /dev/null +++ b/agent/src/issuers/steuern.ts @@ -0,0 +1,158 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { AGENT_HOST } from '../constants' +import type { + CredentialConfigurationDisplay, + MdocConfiguration, + PlaygroundIssuerOptions, + SdJwtConfiguration, +} from '../issuer' +import type { StaticMdocSignInput, StaticSdJwtSignInput } from '../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../utils/date' + +const taxIdDisplay = { + locale: 'en', + name: 'Tax-ID', + text_color: '#525C75', + background_color: '#CAD7E0', + background_image: { + url: `${AGENT_HOST}/assets/issuers/steuern/credential.png`, + uri: `${AGENT_HOST}/assets/issuers/steuern/credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const taxIdPayload = { + tax_number: '06958170437', + affiliation_country: 'DE', + registered_family_name: 'Seiwal', + registered_given_name: 'Ines', + resident_address: 'Gotenring 6, 50667 Köln', + birth_date: new DateOnly('2000-12-12'), + church_tax_ID: 'DE123456789', + iban: 'DE89370400440532013000', + pid_id: 'PID123456789', + + credential_type: 'Tax number', + issuing_authority: 'DE', + issuing_country: 'DE', + + issuance_date: new DateOnly(new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds).toISOString()), + expiry_date: new DateOnly(new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds).toISOString()), +} + +export const taxIdMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'tax-id-mdoc', + doctype: 'eu.europa.ec.eudi.taxid.1', + display: [taxIdDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const taxIdMdocData = { + credentialConfigurationId: 'tax-id-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: taxIdMdoc.doctype, + namespaces: { + [taxIdMdoc.doctype]: taxIdPayload, + }, + validityInfo: { + validFrom: new Date(taxIdPayload.issuance_date.toISOString()), + validUntil: new Date(taxIdPayload.expiry_date.toISOString()), + }, + }, +} satisfies StaticMdocSignInput + +export const taxIdSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'tax-id-sd-jwt', + vct: 'https://example.eudi.ec.europa.eu/tax-id/1', + display: [taxIdDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies SdJwtConfiguration + +export const taxIdSdJwtData = { + credentialConfigurationId: 'tax-id-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + ...taxIdPayload, + birth_date: taxIdPayload.birth_date.toISOString(), + nbf: dateToSeconds(taxIdPayload.issuance_date), + exp: dateToSeconds(taxIdPayload.expiry_date), + issuance_date: taxIdPayload.issuance_date.toISOString(), + expiry_date: taxIdPayload.expiry_date.toISOString(), + vct: taxIdSdJwt.vct, + }, + disclosureFrame: { + _sd: [ + 'tax_number', + 'affiliation_country', + 'registered_family_name', + 'registered_given_name', + 'resident_address', + 'birth_date', + 'church_tax_ID', + 'iban', + 'pid_id', + + 'issuance_date', + 'expiry_date', + 'issuing_authority', + 'issuing_country', + ], + }, + }, +} satisfies StaticSdJwtSignInput + +// https://animosolutions.getoutline.com/doc/certificate-of-residence-attestation-KjzG4n9VG0 +export const steuernIssuer = { + tags: [taxIdDisplay.name], + issuerId: '197625a0-b797-4559-80cc-bf5463b90dc3', + credentialConfigurationsSupported: [ + { + 'vc+sd-jwt': { + configuration: taxIdSdJwt, + data: taxIdSdJwtData, + }, + mso_mdoc: { + configuration: taxIdMdoc, + data: taxIdMdocData, + }, + }, + ], + batchCredentialIssuance: { + batchSize: 10, + }, + display: [ + { + name: 'Bundeszentralamt fur Steuern', + logo: { + url: `${AGENT_HOST}/assets/issuers/steuern/issuer.png`, + uri: `${AGENT_HOST}/assets/issuers/steuern/issuer.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const steuernCredentialsData = { + [taxIdSdJwtData.credentialConfigurationId]: taxIdSdJwtData, + [taxIdMdocData.credentialConfigurationId]: taxIdMdocData, +} diff --git a/agent/src/issuers/telOrg.ts b/agent/src/issuers/telOrg.ts new file mode 100644 index 0000000..48a44cd --- /dev/null +++ b/agent/src/issuers/telOrg.ts @@ -0,0 +1,160 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' + +import { AGENT_HOST } from '../constants' +import type { + CredentialConfigurationDisplay, + MdocConfiguration, + PlaygroundIssuerOptions, + SdJwtConfiguration, +} from '../issuer' +import type { StaticMdocSignInput, StaticSdJwtSignInput } from '../types' +import { + dateToSeconds, + oneYearInMilliseconds, + serverStartupTimeInMilliseconds, + tenDaysInMilliseconds, +} from '../utils/date' + +const msisdnDisplay = { + locale: 'en', + name: 'MSISDN', + text_color: '#000000', + background_color: '#ffb2e1', + background_image: { + url: `${AGENT_HOST}/assets/issuers/telOrg/credential.png`, + uri: `${AGENT_HOST}/assets/issuers/telOrg/credential.png`, + }, +} satisfies CredentialConfigurationDisplay + +const msisdnPayload = { + credential_type: 'MSISDN', + phone_number: '491511234567', + registered_family_name: 'Musterman', + registered_given_name: 'John Michael', + contract_owner: true, + end_user: false, + mobile_operator: 'Telekom_DE', + issuing_organization: 'TelOrg', + verification_date: new DateOnly('2023-08-25'), + verification_method_information: 'NumberVerify', + + issuance_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), +} + +export const msisdnMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'msisdn-mdoc', + doctype: 'eu.europa.ec.eudi.msisdn.1', + display: [msisdnDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const msisdnMdocData = { + credentialConfigurationId: 'msisdn-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: msisdnMdoc.doctype, + namespaces: { + [msisdnMdoc.doctype]: { + ...msisdnPayload, + }, + }, + validityInfo: { + validFrom: msisdnPayload.issuance_date, + validUntil: msisdnPayload.expiry_date, + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: msisdnPayload.issuance_date, + }, + }, +} satisfies StaticMdocSignInput + +export const msisdnSdJwt = { + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'msisdn-sd-jwt', + vct: 'eu.europa.ec.eudi.msisdn.1', + display: [msisdnDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies SdJwtConfiguration + +export const msisdnSdJwtData = { + credentialConfigurationId: 'msisdn-sd-jwt', + format: ClaimFormat.SdJwtVc, + credential: { + payload: { + ...msisdnPayload, + nbf: dateToSeconds(msisdnPayload.issuance_date), + exp: dateToSeconds(msisdnPayload.expiry_date), + issuance_date: msisdnPayload.issuance_date.toISOString(), + expiry_date: msisdnPayload.expiry_date.toISOString(), + vct: msisdnSdJwt.vct, + verification_date: msisdnPayload.verification_date.toISOString(), + }, + disclosureFrame: { + _sd: [ + 'phone_number', + 'registered_family_name', + 'registered_given_name', + 'contract_owner', + 'end_user', + 'mobile_operator', + 'issuing_organization', + 'verification_date', + 'verification_method_information', + 'issuance_date', + 'expiry_date', + ], + }, + }, +} satisfies StaticSdJwtSignInput + +// https://animosolutions.getoutline.com/doc/credential-msisdn-1BljW1GEM0 +export const telOrgIssuer = { + tags: [msisdnDisplay.name], + issuerId: 'a5292f18-3c9f-484a-8515-fb4ec4cb33e8', + credentialConfigurationsSupported: [ + { + 'vc+sd-jwt': { + configuration: msisdnSdJwt, + data: msisdnSdJwtData, + }, + mso_mdoc: { + configuration: msisdnMdoc, + data: msisdnMdocData, + }, + }, + ], + batchCredentialIssuance: { + batchSize: 10, + }, + display: [ + { + name: 'TelOrg', + logo: { + url: `${AGENT_HOST}/assets/issuers/telOrg/issuer.png`, + uri: `${AGENT_HOST}/assets/issuers/telOrg/issuer.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const telOrgCredentialsData = { + [msisdnSdJwtData.credentialConfigurationId]: msisdnSdJwtData, + [msisdnMdocData.credentialConfigurationId]: msisdnMdocData, +} diff --git a/agent/src/issuers/vws.ts b/agent/src/issuers/vws.ts new file mode 100644 index 0000000..6b3d8af --- /dev/null +++ b/agent/src/issuers/vws.ts @@ -0,0 +1,174 @@ +import { ClaimFormat, DateOnly, JwaSignatureAlgorithm } from '@credo-ts/core' +import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' + +import { AGENT_HOST } from '../constants' +import type { CredentialConfigurationDisplay, MdocConfiguration, PlaygroundIssuerOptions } from '../issuer' +import type { StaticMdocSignInput } from '../types' +import { oneYearInMilliseconds, serverStartupTimeInMilliseconds, tenDaysInMilliseconds } from '../utils/date' + +import { over70mDLMdoc, over70mDLMdocData } from './credentials/over70mDLMdoc' +import { age18mDLMdoc, age18mDLMdocData } from './credentials/age18mDLMdoc' + +const eHealthDisplay = { + locale: 'en', + name: 'Vaccination certificate', + text_color: '#525C75', + background_color: '#FFCE00', + background_image: { + url: '', + uri: '', + }, +} satisfies CredentialConfigurationDisplay + +const eHealthPayload = { + fn: 'Mustermann', + gn: 'Erika', + dob: new DateOnly('1964-08-12'), + sex: 2, + v_RA01_1: { + tg: '840539006', + vp: '1119349007', + mp: 'EU/1/20/1528', + ma: 'ORG-100030215', + bn: 'B12345/67', + dn: 1, + sd: 2, + dt: new DateOnly('2021-04-08'), + co: 'UT', + ao: 'RHI', + nx: new DateOnly('2021-05-20'), + is: 'SC17', + ci: 'URN:UVCI:01:UT:187/37512422923', + }, + v_RA01_2: { + tg: '840539006', + vp: '1119349007', + mp: 'EU/1/20/1528', + ma: 'ORG-100030215', + bn: 'B67890/12', + dn: 2, + sd: 2, + dt: new DateOnly('2021-05-18'), + co: 'UT', + ao: 'RHI', + is: 'SC17', + ci: 'URN:UVCI:01:UT:187/37512533044', + }, + pid_PPN: { + pty: 'PPN', + pnr: '476284728', + pic: 'UT', + }, + pid_DL: { + pty: 'DL', + pnr: '987654321', + pic: 'UT', + }, + + issuance_date: new Date(serverStartupTimeInMilliseconds - tenDaysInMilliseconds), + expiry_date: new Date(serverStartupTimeInMilliseconds + oneYearInMilliseconds), +} + +const eHealthPayload_2 = { + '1D47_vaccinated': true, + RA01_vaccinated: true, + RA01_test: { + Result: '260415000', + TypeOfTest: 'LP6464-4', + TimeOfTest: new Date('2021-10-12T19:00:00Z'), + }, + safeEntry_Leisure: { + SeCondFulfilled: true, + SeCondType: 'leisure', + SeCondExpiry: new Date('2021-10-13T19:00:00Z'), + }, + fac: '', + fni: 'M', + gni: 'E', + by: '1964', + bm: '08', + bd: '12', +} + +export const eHealthMdoc = { + format: OpenId4VciCredentialFormatProfile.MsoMdoc, + cryptographic_binding_methods_supported: ['cose_key'], + cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256], + scope: 'e-health-mdoc', + doctype: 'org.micov.vtr.1.', + display: [eHealthDisplay], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + }, + }, +} as const satisfies MdocConfiguration + +export const eHealthMdocData = { + credentialConfigurationId: 'e-health-mdoc', + format: ClaimFormat.MsoMdoc, + credential: { + docType: eHealthMdoc.doctype, + namespaces: { + 'org.micov.vtr.1.': { + ...eHealthPayload, + }, + 'org.micov.attestation.1': { + ...eHealthPayload_2, + }, + }, + validityInfo: { + validFrom: eHealthPayload.issuance_date, + validUntil: eHealthPayload.expiry_date, + + // Causes issue in google identity credential if not present + // Update half year before expiry + expectedUpdate: new Date(serverStartupTimeInMilliseconds + Math.floor(oneYearInMilliseconds / 2)), + signed: eHealthPayload.issuance_date, + }, + }, +} satisfies StaticMdocSignInput + +export const vwsIssuer = { + tags: [eHealthDisplay.name], + issuerId: '23474550-4a4b-4e60-bb3f-fc2a28d68bd5', + credentialConfigurationsSupported: [ + { + mso_mdoc: { + configuration: eHealthMdoc, + data: eHealthMdocData, + }, + }, + { + mso_mdoc: { + configuration: over70mDLMdoc, + data: over70mDLMdocData, + }, + }, + { + mso_mdoc: { + configuration: age18mDLMdoc, + data: age18mDLMdocData, + }, + }, + ], + batchCredentialIssuance: { + batchSize: 10, + }, + display: [ + { + name: 'VWS', + logo: { + // We use the same logo for both issuers Koninkrijk der Nederlanden and VWS + url: `${AGENT_HOST}/assets/issuers/nederlanden/issuer.png`, + uri: `${AGENT_HOST}/assets/issuers/nederlanden/issuer.png`, + }, + }, + ], +} satisfies PlaygroundIssuerOptions + +export const vwsCredentialsData = { + [eHealthMdocData.credentialConfigurationId]: eHealthMdocData, + [over70mDLMdocData.credentialConfigurationId]: over70mDLMdocData, + [age18mDLMdocData.credentialConfigurationId]: age18mDLMdocData, +} diff --git a/agent/src/keyMethods/createKeys.ts b/agent/src/keyMethods/createKeys.ts new file mode 100644 index 0000000..a4fbaf0 --- /dev/null +++ b/agent/src/keyMethods/createKeys.ts @@ -0,0 +1,17 @@ +import { KeyType, TypedArrayEncoder } from '@credo-ts/core' +import { agent } from '../agent' +import { DCS_P256_SEED, ROOT_P256_SEED } from '../constants' + +export async function createKeys() { + const authorityKey = await agent.wallet.createKey({ + keyType: KeyType.P256, + seed: TypedArrayEncoder.fromString(ROOT_P256_SEED), + }) + + const documentSignerKey = await agent.wallet.createKey({ + keyType: KeyType.P256, + seed: TypedArrayEncoder.fromString(DCS_P256_SEED), + }) + + return { authorityKey, documentSignerKey } +} diff --git a/agent/src/keyMethods/createSelfSignedCertificate.ts b/agent/src/keyMethods/createSelfSignedCertificate.ts new file mode 100644 index 0000000..87d02f1 --- /dev/null +++ b/agent/src/keyMethods/createSelfSignedCertificate.ts @@ -0,0 +1,145 @@ +import { type Key, type X509Certificate, X509ExtendedKeyUsage, X509KeyUsage, X509Service } from '@credo-ts/core' +import { agent } from '../agent' +import { AGENT_DNS, AGENT_HOST } from '../constants' + +import { type AgentContext, CredoWebCrypto, CredoWebCryptoKey } from '@credo-ts/core' +import { credoKeyTypeIntoCryptoKeyAlgorithm } from '@credo-ts/core/build/crypto/webcrypto/utils/keyAlgorithmConversion' +import * as x509 from '@peculiar/x509' +import { tenDaysInMilliseconds } from '../utils/date' + +/** + * Creates a Certificate Revocation List (CRL) for the agent. + * + * @throws Will throw an error if the CRL creation process fails. + */ +export async function createCertificateRevocationList({ + entityName, + context, + key, +}: { + entityName: string + context: AgentContext + key: Key +}) { + try { + const webCrypto = new CredoWebCrypto(context) + const cryptoKeyAlgorithm = credoKeyTypeIntoCryptoKeyAlgorithm(key.keyType) + const privateKey = new CredoWebCryptoKey(key, cryptoKeyAlgorithm, false, 'private', ['sign']) + const publicKey = new CredoWebCryptoKey(key, cryptoKeyAlgorithm, true, 'public', ['sign']) + context.config.logger.info('Creating Certificate Revocation List') + const authorityKeyIdentifierExtension = await x509.AuthorityKeyIdentifierExtension.create( + publicKey, + false, // mark extension as non-critical + webCrypto + ) + const crlNumberExtension = new x509.Extension( + '2.5.29.20', // CRL Number OID + false, // mark extension as non-critical + new Uint8Array([0x02, 0x01, 0x01]) // ASN.1 INTEGER with value 1 + ) + const crl = await x509.X509CrlGenerator.create( + { + signingKey: privateKey, + issuer: `CN=${entityName},C=NL`, + thisUpdate: new Date(), + nextUpdate: new Date(Date.now() + 360 * 24 * 60 * 60 * 1000), + extensions: [authorityKeyIdentifierExtension, crlNumberExtension], + entries: [], + signingAlgorithm: { + name: 'ECDSA', + hash: { name: 'SHA-256' }, + }, + }, + webCrypto + ) + agent.config.logger.info('Certificate Revocation List created') + + return crl + } catch (error) { + agent.config.logger.error('Error creating Certificate Revocation List', { + error, + }) + throw new Error(`Error creating Certificate Revocation List: ${error}`) + } +} + +export const createRootCertificate = async (key: Key) => { + const lastYear = new Date() + const nextYear = new Date() + nextYear.setFullYear(nextYear.getFullYear() + 3) + lastYear.setFullYear(lastYear.getFullYear() - 1) + + return X509Service.createCertificate(agent.context, { + authorityKey: key, + issuer: { countryName: 'NL', commonName: 'Animo' }, + validity: { + notBefore: lastYear, + notAfter: nextYear, + }, + extensions: { + subjectKeyIdentifier: { + include: true, + }, + keyUsage: { + usages: [X509KeyUsage.KeyCertSign, X509KeyUsage.CrlSign], + markAsCritical: true, + }, + issuerAlternativeName: { + name: [{ type: 'url', value: AGENT_HOST }], + }, + basicConstraints: { + ca: true, + pathLenConstraint: 0, + markAsCritical: true, + }, + crlDistributionPoints: { + urls: [`${AGENT_HOST}/crl`], + }, + }, + }) +} + +export const createDocumentSignerCertificate = async ( + authorityKey: Key, + subjectKey: Key, + rootCertificate: X509Certificate +) => { + const notBefore = new Date(Date.now() - tenDaysInMilliseconds * 2) + + const nextYear = new Date() + nextYear.setFullYear(nextYear.getFullYear() + 1) + + return X509Service.createCertificate(agent.context, { + authorityKey, + subjectPublicKey: subjectKey, + issuer: rootCertificate.issuer, + subject: { commonName: 'credo dcs', countryName: 'NL' }, + validity: { + notBefore, + notAfter: nextYear, + }, + extensions: { + authorityKeyIdentifier: { + include: true, + }, + subjectKeyIdentifier: { + include: true, + }, + keyUsage: { + usages: [X509KeyUsage.DigitalSignature], + markAsCritical: true, + }, + subjectAlternativeName: { + name: [{ type: 'dns', value: AGENT_DNS }], + }, + issuerAlternativeName: { + // biome-ignore lint/style/noNonNullAssertion: + name: rootCertificate.issuerAlternativeNames!, + }, + extendedKeyUsage: { + usages: [X509ExtendedKeyUsage.MdlDs], + markAsCritical: true, + }, + }, + }) +} diff --git a/agent/src/keyMethods/index.ts b/agent/src/keyMethods/index.ts new file mode 100644 index 0000000..5a774a6 --- /dev/null +++ b/agent/src/keyMethods/index.ts @@ -0,0 +1 @@ +export * from './setup' diff --git a/agent/src/keyMethods/setup.ts b/agent/src/keyMethods/setup.ts new file mode 100644 index 0000000..3205022 --- /dev/null +++ b/agent/src/keyMethods/setup.ts @@ -0,0 +1,132 @@ +import { type Logger, WalletKeyExistsError, X509Certificate, X509Service } from '@credo-ts/core' +import { agent } from '../agent' +import { X509_DCS_CERTIFICATE, X509_ROOT_CERTIFICATE } from '../constants' +import { createKeys } from './createKeys' +import { + createCertificateRevocationList, + createDocumentSignerCertificate, + createRootCertificate, +} from './createSelfSignedCertificate' + +let x509RootCertificate: string | undefined = undefined +let x509DcsCertificate: string | undefined = undefined +let crl: Buffer | undefined = undefined + +export async function setupX509Certificate() { + const x509Record = await agent.genericRecords.findById('X509_CERTIFICATE') + + try { + const { documentSignerKey, authorityKey } = await createKeys() + + if (X509_ROOT_CERTIFICATE) { + const parsedCertificate = X509Service.parseCertificate(agent.context, { + encodedCertificate: X509_ROOT_CERTIFICATE, + }) + x509RootCertificate = parsedCertificate.toString('base64') + + if ( + parsedCertificate.publicKey.keyType !== authorityKey.keyType || + !Buffer.from(parsedCertificate.publicKey.publicKey).equals(Buffer.from(authorityKey.publicKey)) + ) { + throw new Error( + 'Key in provided X509_ROOT_CERTIFICATE env variable does not match the key from the ROOT_P256_SEED' + ) + } + if (X509_DCS_CERTIFICATE) { + const parsedCertificate = X509Service.parseCertificate(agent.context, { + encodedCertificate: X509_DCS_CERTIFICATE, + }) + x509DcsCertificate = parsedCertificate.toString('base64') + + if ( + parsedCertificate.publicKey.keyType !== documentSignerKey.keyType || + !Buffer.from(parsedCertificate.publicKey.publicKey).equals(Buffer.from(documentSignerKey.publicKey)) + ) { + throw new Error( + 'Key in provided X509_DCS_CERTIFICATE env variable does not match the key from the DCS_P256_SEED' + ) + } + } else { + const dcsCertificate = await createDocumentSignerCertificate(authorityKey, documentSignerKey, parsedCertificate) + x509DcsCertificate = dcsCertificate.toString('base64') + } + } else { + const rootCertificate = await createRootCertificate(authorityKey) + const dcsCertificate = await createDocumentSignerCertificate(authorityKey, documentSignerKey, rootCertificate) + x509RootCertificate = rootCertificate.toString('base64') + x509DcsCertificate = dcsCertificate.toString('base64') + } + + if (x509Record) { + x509Record.content = { root: x509RootCertificate, dcs: x509DcsCertificate } + await agent.genericRecords.update(x509Record) + } else { + await agent.genericRecords.save({ + id: 'X509_CERTIFICATE', + content: { root: x509RootCertificate, dcs: x509DcsCertificate }, + }) + } + console.log(x509DcsCertificate, x509RootCertificate) + } catch (error) { + // If the key already exists, we assume the self-signed certificate is already created + if (error instanceof WalletKeyExistsError) { + if (!x509Record) { + throw new Error('No available key method record found') + } + x509RootCertificate = x509Record.content.root as string + x509DcsCertificate = x509Record.content.dcs as string + } else { + throw error + } + } + + const crlInstance = await createCertificateRevocationList({ + context: agent.context, + entityName: 'Animo', + key: X509Certificate.fromEncodedCertificate(x509RootCertificate).publicKey, + }) + + crl = Buffer.from(crlInstance.rawData) + + console.log('======= X.509 IACA ROOT Certificate ===========') + console.log(x509RootCertificate) + + console.log('======= X.509 IACA DCS Certificate ===========') + console.log(x509DcsCertificate) + + console.log('======= X.509 CRL ===========') + console.log(crlInstance.toString('pem')) + + if (!x509RootCertificate || !x509DcsCertificate) { + throw new Error('Error setting up certificates') + } + + agent.x509.addTrustedCertificate(x509RootCertificate) + agent.x509.addTrustedCertificate(x509RootCertificate) +} + +export function getX509RootCertificate() { + if (!x509RootCertificate) { + throw new Error('X509 root certificate is not setup properly') + } + return x509RootCertificate +} + +export function getCertificateRevocationList() { + if (!crl) { + throw new Error('X509 Certificate Revocation List is not setup properly') + } + + return crl +} + +export function getX509DcsCertificate() { + if (!x509DcsCertificate) { + throw new Error('X509 dcs certificate is not setup properly') + } + return x509DcsCertificate +} + +export function getX509Certificates() { + return [getX509DcsCertificate(), getX509RootCertificate()] +} diff --git a/agent/src/oidcProvider/provider.ts b/agent/src/oidcProvider/provider.ts new file mode 100644 index 0000000..0801fd1 --- /dev/null +++ b/agent/src/oidcProvider/provider.ts @@ -0,0 +1,124 @@ +import * as express from 'express' +import { AGENT_HOST } from '../constants' +import { issuers as _issuers } from '../issuers' + +const issuers = _issuers.map((issuer) => ({ + ...issuer, + scopes: Object.values(issuer.credentialConfigurationsSupported).flatMap((s) => + Object.values(s).map((c) => c.configuration.scope as string) + ), + issuerUrl: `${AGENT_HOST}/oid4vci/${issuer.issuerId}`, +})) + +const oidcRouterPath = '/provider' +const oidcRouter = express.Router() + +// I can't figure out how to bind a custom request parameter to the session +// so it can be bound to the access token. This is a very hacky 'global' issuer_state +// and only works if only person is authenticating. Of course very unsecure, but it's a demo +let issuer_state: string | undefined = undefined + +async function getProvider() { + const { Provider, errors } = await import('oidc-provider') + const oidc = new Provider(`${AGENT_HOST}${oidcRouterPath}`, { + clientAuthMethods: ['client_secret_basic', 'client_secret_post', 'none'], + clients: [ + { + client_id: 'wallet', + client_secret: 'wallet', + grant_types: ['authorization_code'], + id_token_signed_response_alg: 'ES256', + redirect_uris: ['io.mosip.residentapp.inji://oauthredirect'], + application_type: 'native', + }, + { + client_id: 'issuer-server', + client_secret: 'issuer-server', + id_token_signed_response_alg: 'ES256', + redirect_uris: [], + }, + ], + jwks: { + keys: [ + { + alg: 'ES256', + kid: 'first-key', + kty: 'EC', + d: '2hdTKWEZza_R-DF4l3aoWEuGZPy6L6PGmUT_GqeJczM', + crv: 'P-256', + x: '73lW9QyiXTvpOOXuT_LoRRvM3oEWKSLyzfNGe04sV5k', + y: 'AiFefLdnP-cWkdsevwozKdxNGvF_VSSZ1K5yDQ4jWwM', + }, + ], + }, + scopes: [], + pkce: { + methods: ['S256'], + required: () => true, + }, + extraTokenClaims: async (context, token) => { + if (token.kind === 'AccessToken') { + return { + issuer_state, + } + } + return undefined + }, + clientBasedCORS: () => true, + extraParams: { + issuer_state: (_, value) => { + issuer_state = value + }, + }, + features: { + dPoP: { enabled: true }, + pushedAuthorizationRequests: { + enabled: true, + requirePushedAuthorizationRequests: false, + allowUnregisteredRedirectUris: true, + }, + introspection: { + enabled: true, + }, + resourceIndicators: { + // TODO: default resource? + // defaultResource: () => issuers[0].issuerUrl, + enabled: true, + getResourceServerInfo: (context, resourceIndicator) => { + const issuer = issuers.find((issuer) => issuer.issuerUrl === resourceIndicator) + if (!issuer) throw new errors.InvalidTarget() + + return { + scope: issuer.scopes.join(' '), + accessTokenTTL: 5 * 60, // 5 minutes + accessTokenFormat: 'jwt', + // TODO: detect resource? + audience: issuer.issuerUrl, + jwt: { + sign: { + kid: 'first-key', + alg: 'ES256', + }, + }, + } + }, + }, + }, + + async findAccount(_, id) { + return { + accountId: id, + async claims() { + return { sub: id } + }, + } + }, + }) + + oidc.proxy = true + + return oidc +} + +const oidcUrl = `${AGENT_HOST}${oidcRouterPath}` +export { oidcRouter, getProvider, oidcRouterPath, oidcUrl } diff --git a/agent/src/server.ts b/agent/src/server.ts index cb3419b..49551c8 100644 --- a/agent/src/server.ts +++ b/agent/src/server.ts @@ -1,47 +1,159 @@ -import { agent, openId4VciRouter, openId4VpRouter } from "./agent"; -import { apiRouter } from "./endpoints"; -import { createIssuer, doesIssuerExist, updateIssuer } from "./issuer"; -import { createVerifier, doesVerifierExist } from "./verifier"; -import express, { Response } from "express"; -import { getWebDidDocument, setupAllDids } from "./did"; -import cors from "cors"; - +import path from 'path' +import { JwaSignatureAlgorithm, KeyType } from '@credo-ts/core' +import cors from 'cors' +import express from 'express' +import type { Response } from 'express' +import { agent, openId4VciRouter, openId4VpRouter } from './agent' +import { AGENT_HOST } from './constants' +import { createDidWeb, getWebDidDocument } from './didWeb' +import { apiRouter } from './endpoints' +import { type PlaygroundIssuerOptions, createOrUpdateIssuer } from './issuer' +import { issuers } from './issuers' +import { getCertificateRevocationList, setupX509Certificate } from './keyMethods' +import { getProvider, oidcRouterPath, oidcUrl } from './oidcProvider/provider' +import { createOrUpdateVerifier } from './verifier' +import { verifiers } from './verifiers' async function run() { - await agent.initialize(); + await agent.initialize() + + for (const issuer of issuers as PlaygroundIssuerOptions[]) { + const { tags, credentialConfigurationsSupported, ...restIssuer } = issuer + await createOrUpdateIssuer({ + ...restIssuer, + credentialConfigurationsSupported: Object.fromEntries( + credentialConfigurationsSupported.flatMap((item) => + Object.values(item).flatMap((itemitem) => [ + [itemitem.data.credentialConfigurationId, itemitem.configuration], + [ + `${itemitem.data.credentialConfigurationId}-key-attestations`, + { ...itemitem.configuration, proof_types_supported: {} }, + ], + ...(itemitem.configuration.format === 'vc+sd-jwt' + ? [ + [ + `${itemitem.data.credentialConfigurationId}-dc-sd-jwt`, + { ...itemitem.configuration, format: 'dc+sd-jwt' }, + ], + [ + `${itemitem.data.credentialConfigurationId}-dc-sd-jwt-key-attestations`, + { + ...itemitem.configuration, + format: 'dc+sd-jwt', + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + key_attestations_required: { + user_authentication: ['iso_18045_high'], + key_storage: ['iso_18045_high'], + }, + }, + attestation: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + key_attestations_required: { + user_authentication: ['iso_18045_high'], + key_storage: ['iso_18045_high'], + }, + }, + }, + }, + ], + ] + : []), + ...(itemitem.configuration.format !== 'ldp_vc' + ? [ + [ + `${itemitem.data.credentialConfigurationId}-key-attestations`, + { + ...itemitem.configuration, + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + key_attestations_required: { + user_authentication: ['iso_18045_high'], + key_storage: ['iso_18045_high'], + }, + }, + attestation: { + proof_signing_alg_values_supported: [JwaSignatureAlgorithm.ES256], + key_attestations_required: { + user_authentication: ['iso_18045_high'], + key_storage: ['iso_18045_high'], + }, + }, + }, + }, + ], + ] + : []), + ]) + ) + ), + dpopSigningAlgValuesSupported: [JwaSignatureAlgorithm.ES256], + accessTokenSignerKeyType: KeyType.P256, + authorizationServerConfigs: [ + { + issuer: oidcUrl, + clientAuthentication: { + clientId: 'issuer-server', + clientSecret: 'issuer-server', + }, + }, + ], + }) + } - if (!(await doesIssuerExist())) { - await createIssuer(); - } else { - // We update the issuer metadata on every startup to sync the static issuer metadata with the issuer metadata record - await updateIssuer(); + for (const verifier of verifiers) { + await createOrUpdateVerifier(verifier) } - if (!(await doesVerifierExist())) { - await createVerifier(); + await setupX509Certificate() + await getWebDidDocument().catch(async () => { + const key = await agent.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + await createDidWeb(key) + }) + + const app = express() + app.use(cors({ origin: '*' })) + app.use(express.json()) + app.use(express.urlencoded()) + + // Hack for making images available + if (AGENT_HOST.includes('ngrok') || AGENT_HOST.includes('.ts.net') || AGENT_HOST.includes('localhost')) { + console.log(path.join(__dirname, '../../app/public/assets')) + app.use('/assets', express.static(path.join(__dirname, '../../app/public/assets'))) } - await setupAllDids(); + app.use('/oid4vci', openId4VciRouter) + app.use('/oid4vp', openId4VpRouter) + app.use('/api', apiRouter) + app.use((request, _, next) => { + if (request.path === '/provider/request' || request.path === '/provider/token') { + request.body.client_id = 'wallet' + request.body.client_secret = 'wallet' + } + next() + }) + app.use('/.well-known/did.json', async (_, response: Response) => { + const didWeb = await getWebDidDocument() + return response.json(didWeb.toJSON()) + }) - const app = express(); - app.use(cors({ origin: "*" })); + app.use('/crl', async (_, response) => { + return response.contentType('application/pkix-crl').send(getCertificateRevocationList()) + }) - app.use("/oid4vci", openId4VciRouter); - app.use("/siop", openId4VpRouter); - app.use("/api", apiRouter); - app.use("/.well-known/did.json", async (_, response: Response) => { - const didWeb = await getWebDidDocument(); - return response.json(didWeb.toJSON()); - }); + const oidc = await getProvider() + app.use(oidcRouterPath, oidc.callback()) - app.listen(3001, () => - agent.config.logger.info("app listening on port 3001") - ); + app.listen(3001, () => agent.config.logger.info('app listening on port 3001')) // @ts-ignore app.use((err, _, res, __) => { - console.error(err.stack); - res.status(500).send("Something broke!"); - }); + console.error(err.stack) + res.status(500).send('Something broke!') + }) } -run(); +run() diff --git a/agent/src/session.ts b/agent/src/session.ts index 537ec44..7b81129 100644 --- a/agent/src/session.ts +++ b/agent/src/session.ts @@ -1,3 +1,3 @@ export interface OfferSessionMetadata { - issuerDidMethod: string; + issuerDidMethod: string } diff --git a/agent/src/types.ts b/agent/src/types.ts new file mode 100644 index 0000000..81c9cf9 --- /dev/null +++ b/agent/src/types.ts @@ -0,0 +1,21 @@ +import type { + OpenId4VciCredentialConfigurationSupported, + OpenId4VciSignMdocCredentials, + OpenId4VciSignSdJwtCredentials, + OpenId4VciSignW3cCredentials, +} from '@credo-ts/openid4vc' + +export type CredentialDisplay = NonNullable[number] +export type StaticSdJwtSignInput = { + credential: Omit + credentialConfigurationId: string +} & Omit +export type StaticMdocSignInput = { + credential: Omit + credentialConfigurationId: string +} & Omit + +export type StaticLdpVcSignInput = { + credential: Omit + credentialConfigurationId: string +} & Omit diff --git a/agent/src/utils/LimitedSizeCollection.ts b/agent/src/utils/LimitedSizeCollection.ts new file mode 100644 index 0000000..261a749 --- /dev/null +++ b/agent/src/utils/LimitedSizeCollection.ts @@ -0,0 +1,23 @@ +export class LimitedSizeCollection { + private map: Map = new Map() + + constructor(private maxSize = 500) {} + + set(key: string, value: T) { + // Add or update the entry + this.map.set(key, value) + + // If we've exceeded the maximum size, remove the oldest entry + if (this.map.size > this.maxSize) { + // Get the first key (oldest) using the iterator + const oldestKey = this.map.keys().next().value + if (oldestKey) this.map.delete(oldestKey) + } + + return this + } + + get(key: string) { + return this.map.get(key) + } +} diff --git a/agent/src/utils/date.ts b/agent/src/utils/date.ts new file mode 100644 index 0000000..3c61ac6 --- /dev/null +++ b/agent/src/utils/date.ts @@ -0,0 +1,11 @@ +import { DateOnly } from '@credo-ts/core' + +export const oneDayInMilliseconds = 24 * 60 * 60 * 1000 +export const tenDaysInMilliseconds = 10 * oneDayInMilliseconds +export const oneYearInMilliseconds = 365 * oneDayInMilliseconds +export const serverStartupTimeInMilliseconds = Date.now() + +export function dateToSeconds(date: Date | DateOnly) { + const realDate = date instanceof DateOnly ? new Date(date.toISOString()) : date + return Math.floor(realDate.getTime() / 1000) +} diff --git a/agent/src/utils/image.ts b/agent/src/utils/image.ts new file mode 100644 index 0000000..e6650aa --- /dev/null +++ b/agent/src/utils/image.ts @@ -0,0 +1,21 @@ +import fs from 'fs' + +// TODO: we should use this in the Pardaym wallet to detect an image in a field. +// Function to check if buffer is a JPEG image +function isJPEG(buffer: Buffer) { + // JPEG files start with FF D8 and end with FF D9 + return ( + buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[buffer.length - 2] === 0xff && buffer[buffer.length - 1] === 0xd9 + ) +} + +// Sync version if you prefer +export function loadJPEGBufferSync(filePath: string) { + const buffer = fs.readFileSync(filePath) + + if (!isJPEG(buffer)) { + throw new Error('File is not a valid JPEG image') + } + + return buffer +} diff --git a/agent/src/verifier.ts b/agent/src/verifier.ts index 06545fb..9894821 100644 --- a/agent/src/verifier.ts +++ b/agent/src/verifier.ts @@ -1,22 +1,47 @@ -import { agent } from "./agent"; +import type { OpenId4VpCreateVerifierOptions } from '@credo-ts/openid4vc' +import { agent } from './agent' +import type { MdocCredential, SdJwtCredential } from './verifiers/util' -const verifierId = "c01ea0f3-34df-41d5-89d1-50ef3d181855"; +export interface PlaygroundVerifierOptions { + verifierId: string + clientMetadata?: OpenId4VpCreateVerifierOptions['clientMetadata'] + requests: Array<{ + name: string + purpose: string + credentials: Array + // Indexes + credential_sets?: Array + }> + useCase?: { + name: string + icon: string + tags: Array + } +} -export async function createVerifier() { - return agent.modules.openId4VcVerifier.createVerifier({ - verifierId, - }); +export async function createOrUpdateVerifier(options: PlaygroundVerifierOptions) { + if (await doesVerifierExist(options.verifierId)) { + await agent.modules.openId4VcVerifier.updateVerifierMetadata({ + verifierId: options.verifierId, + clientMetadata: options.clientMetadata, + }) + } else { + return agent.modules.openId4VcVerifier.createVerifier({ + clientMetadata: options.clientMetadata, + verifierId: options.verifierId, + }) + } } -export async function doesVerifierExist() { +export async function doesVerifierExist(verifierId: string) { try { - await agent.modules.openId4VcVerifier.getVerifierByVerifierId(verifierId); - return true; + await agent.modules.openId4VcVerifier.getVerifierByVerifierId(verifierId) + return true } catch (error) { - return false; + return false } } -export async function getVerifier() { - return agent.modules.openId4VcVerifier.getVerifierByVerifierId(verifierId); +export async function getVerifier(verifierId: string) { + return agent.modules.openId4VcVerifier.getVerifierByVerifierId(verifierId) } diff --git a/agent/src/verifiers/bundesregierung.ts b/agent/src/verifiers/bundesregierung.ts new file mode 100644 index 0000000..b67d5d3 --- /dev/null +++ b/agent/src/verifiers/bundesregierung.ts @@ -0,0 +1,294 @@ +import { AGENT_HOST } from '../constants' +import { arfCompliantPidSdJwt, arfCompliantPidUrnVctSdJwt, mobileDriversLicenseMdoc } from '../issuers/bdr' +import { taxIdMdoc, taxIdSdJwt } from '../issuers/steuern' +import type { PlaygroundVerifierOptions } from '../verifier' +import { pidMdocCredential, pidSdJwtCredential } from './util' + +export const bundesregierungVerifier = { + verifierId: '019368ed-3787-7669-b7f4-8c012238e90d', + useCase: { + name: 'Government identification', + icon: 'government', + tags: ['PID', 'Present Multiple Credentials', 'mixed-credentials', 'Query languages', 'Federation support'], + }, + + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/bunde.png`, + client_name: 'Die Bundesregierung', + }, + + requests: [ + { + name: 'MDL (mdoc)', + purpose: 'Authorize to the government using your mobile drivers license', + credentials: [ + { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: [ + 'given_name', + 'family_name', + 'birth_date', + 'document_number', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'driving_privileges', + ], + }, + ], + }, + { + name: 'Tax-ID two formats (in both sd-jwt vc and mso_mdoc)', + purpose: 'We need to verify your tax number and address', + credentials: [ + { + format: 'mso_mdoc', + doctype: taxIdMdoc.doctype, + namespace: taxIdMdoc.doctype, + fields: ['resident_address', 'issuance_date'], + }, + { + format: 'dc+sd-jwt', + vcts: [taxIdSdJwt.vct], + fields: ['credential_type', 'resident_address', 'birth_date'], + }, + ], + }, + { + name: 'ARF PID (sd-jwt vc) - Most', + purpose: 'To grant you access we need to verify your ARF compliant PID', + credentials: [ + { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct, arfCompliantPidUrnVctSdJwt.vct], + fields: [ + // Mandatory + 'family_name', + 'given_name', + 'birth_date', + 'age_over_18', + + // Mandatory metadata + 'issuance_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + + // Optional + 'age_over_12', + 'age_over_14', + 'age_over_16', + 'age_over_21', + 'age_over_65', + 'age_in_years', + 'age_birth_year', + 'family_name_birth', + 'birth_place', + 'resident_country', + 'resident_city', + 'resident_postal_code', + 'resident_street', + 'nationality', + ], + }, + ], + }, + { + name: 'ARF 1.5 PID (sd-jwt vc) - Mandatory', + purpose: 'To grant you access we need to verify your ARF compliant PID', + + credentials: [ + { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct, arfCompliantPidUrnVctSdJwt.vct], + fields: [ + // Mandatory + 'family_name', + 'given_name', + 'birth_date', + 'age_over_18', + + // Mandatory metadata + 'issuance_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + ], + }, + ], + }, + + { + name: 'BDR PID (sd-jwt vc) - Names', + purpose: 'Please sign this document', + credentials: [ + pidSdJwtCredential({ + fields: ['family_name', 'given_name'], + }), + ], + }, + { + name: 'ARF 1.5 PID (sd-jwt vc) - Names', + purpose: 'To grant you access we need to verify your ARF compliant PID', + + credentials: [ + { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct, arfCompliantPidUrnVctSdJwt.vct], + fields: [ + // Mandatory + 'family_name', + 'given_name', + ], + }, + ], + }, + { + name: 'ARF PID (mdoc) - Most', + purpose: 'To grant you access we need to verify your PID', + + credentials: [ + pidMdocCredential({ + fields: [ + // Mandatory + 'family_name', + 'given_name', + 'birth_date', + 'age_over_18', + + // Mandatory metadata + 'issuance_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + + // Optional + 'age_over_12', + 'age_over_14', + 'age_over_16', + 'age_over_21', + 'age_over_65', + 'age_in_years', + 'age_birth_year', + 'family_name_birth', + 'birth_place', + 'resident_country', + 'resident_city', + 'resident_postal_code', + 'resident_street', + 'nationality', + ], + }), + ], + }, + { + name: 'ARF PID (mdoc) - Mandatory', + purpose: 'To grant you access we need to verify your ARF compliant PID', + + credentials: [ + pidMdocCredential({ + fields: [ + // Mandatory + 'family_name', + 'given_name', + 'birth_date', + 'age_over_18', + + // Mandatory metadata + 'issuance_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + ], + }), + ], + }, + { + name: 'ARF PID (mdoc) - Names', + purpose: 'To grant you access we need to verify your ARF compliant PID', + + credentials: [ + pidMdocCredential({ + fields: [ + // Mandatory + 'family_name', + 'given_name', + ], + }), + ], + }, + { + name: 'mDL (mdoc) - Mandatory', + purpose: 'To grant you access we need to verify your drivers license', + credentials: [ + { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: [ + 'given_name', + 'family_name', + 'birth_date', + 'document_number', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'driving_privileges', + ], + }, + ], + }, + { + name: 'mDL (mdoc) - Names', + purpose: 'To grant you access we need to verify your drivers license', + credentials: [ + { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: ['given_name', 'family_name'], + }, + ], + }, + { + name: 'ARF 1.8 PID (sd-jwt-vc) - Names', + purpose: 'To grant you access we need to verify your ARF compliant PID', + + credentials: [ + { + format: 'dc+sd-jwt', + vcts: ['urn:eudi:pid:1'], + fields: ['family_name', 'given_name'], + }, + ], + }, + { + name: 'ARF 1.8 PID (sd-jwt-vc) - Mandatory', + purpose: 'To grant you access we need to verify your ARF compliant PID', + + credentials: [ + { + format: 'dc+sd-jwt', + vcts: ['urn:eudi:pid:1'], + fields: [ + // Mandatory + 'family_name', + 'given_name', + 'birthdate', + 'place_of_birth.country', + 'nationalities', + + // Mandatory metadata + 'date_of_expiry', + 'issuing_country', + 'issuing_authority', + ], + }, + ], + }, + ], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/cheapCars.ts b/agent/src/verifiers/cheapCars.ts new file mode 100644 index 0000000..27a68ba --- /dev/null +++ b/agent/src/verifiers/cheapCars.ts @@ -0,0 +1,34 @@ +import { AGENT_HOST } from '../constants' +import { mobileDriversLicenseSdJwt } from '../issuers/bdr' +import type { PlaygroundVerifierOptions } from '../verifier' +import { pidSdJwtCredential } from './util' + +export const cheapCarsVerifier = { + verifierId: '019368fe-ee82-7990-880c-7f0ceb92b0aa', + useCase: { + name: 'Rent a car', + icon: 'car-rental', + tags: ['multi-credentials', 'mixed-credentials', 'Query languages', 'Federation support'], + }, + + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/cheap-cars.webp`, + client_name: 'Cheap Cars', + }, + requests: [ + { + name: 'PID and MDL (sd-jwt vc) - Not trust anchor - AI over asking', + purpose: 'To secure your car reservations and finalize the transaction, we require the following attributes', + credentials: [ + { + format: 'dc+sd-jwt', + vcts: [mobileDriversLicenseSdJwt.vct], + fields: ['document_number', 'portrait', 'issue_date', 'expiry_date', 'issuing_country', 'issuing_authority'], + }, + pidSdJwtCredential({ + fields: ['given_name', 'family_name', 'birthdate', 'address.country', 'nationalities'], + }), + ], + }, + ], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/deineBank.ts b/agent/src/verifiers/deineBank.ts new file mode 100644 index 0000000..301db9a --- /dev/null +++ b/agent/src/verifiers/deineBank.ts @@ -0,0 +1,106 @@ +import { AGENT_HOST } from '../constants' +import type { PlaygroundVerifierOptions } from '../verifier' +import { pidMdocCredential, pidSdJwtCredential } from './util' + +export const deineBankVerifier = { + verifierId: '044721ed-af79-45ec-bab3-de85c3e722d0', + useCase: { + name: 'Open a bank account', + icon: 'bank', + tags: ['Federation support', 'Smart AI warnings', 'multi-credentials', 'mixed-credentials'], + }, + + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/deinebank.png`, + client_name: 'DeineBank', + }, + requests: [ + { + name: 'DeineBank.de', + purpose: + 'Um ein Konto bei DeineBank zu eröffnen, müssen wir Ihren Namen, Ihr Geburtsdatum, Ihren Wohnsitz und Ihre Staatsangehörigkeit überprüfen.', + credentials: [ + pidSdJwtCredential({ + fields: [ + 'given_name', + 'family_name', + 'birth_family_name', + 'birthdate', + 'age_equal_or_over.18', + 'place_of_birth.locality', + 'address.locality', + 'address.postal_code', + 'address.street_address', + 'nationalities', + ], + }), + ], + }, + // This one will include the RP access and registration certificate + { + name: 'RP A&A - DeineBank.de', + purpose: + 'Um ein Konto bei DeineBank zu eröffnen, müssen wir Ihren Namen, Ihr Geburtsdatum, Ihren Wohnsitz und Ihre Staatsangehörigkeit überprüfen.', + credentials: [ + pidSdJwtCredential({ + fields: [ + 'given_name', + 'family_name', + 'birth_family_name', + 'birthdate', + 'age_equal_or_over.18', + 'place_of_birth.locality', + 'address.locality', + 'address.postal_code', + 'address.street_address', + 'nationalities', + ], + }), + ], + }, + { + name: 'DeineBank.de', + purpose: + 'Um ein Konto bei DeineBank zu eröffnen, müssen wir Ihren Namen, Ihr Geburtsdatum, Ihren Wohnsitz und Ihre Staatsangehörigkeit überprüfen.', + + credentials: [ + pidMdocCredential({ + fields: [ + 'family_name', + 'given_name', + 'birth_date', + 'nationality', + 'resident_country', + 'resident_city', + 'resident_postal_code', + 'resident_street', + ], + }), + ], + }, + { + name: 'Overasking - DeineBank.de', + purpose: + 'Um ein Konto bei DeineBank zu eröffnen, müssen wir Ihren Namen, Ihr Geburtsdatum, Ihren Wohnsitz und Ihre Staatsangehörigkeit überprüfen.', + + credentials: [ + pidMdocCredential({ + fields: [ + 'family_name', + 'given_name', + 'birth_date', + 'nationality', + 'resident_country', + 'resident_city', + 'resident_postal_code', + 'resident_street', + + 'birth_place', + 'age_over_18', + 'family_name_birth', + ], + }), + ], + }, + ], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/europeanUnion.ts b/agent/src/verifiers/europeanUnion.ts new file mode 100644 index 0000000..156db8c --- /dev/null +++ b/agent/src/verifiers/europeanUnion.ts @@ -0,0 +1,11 @@ +import { AGENT_HOST } from '../constants' +import type { PlaygroundVerifierOptions } from '../verifier' + +export const europeanUnionVerifier = { + verifierId: '01936907-56a3-7007-a61f-44bff8b5d175', + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/eu.png`, + client_name: 'European Union', + }, + requests: [], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/farmatec.ts b/agent/src/verifiers/farmatec.ts new file mode 100644 index 0000000..b188573 --- /dev/null +++ b/agent/src/verifiers/farmatec.ts @@ -0,0 +1,11 @@ +import { AGENT_HOST } from '../constants' +import type { PlaygroundVerifierOptions } from '../verifier' + +export const farmatecVerifier = { + verifierId: '01936904-6f3c-7ccd-9e80-63e6d4945d93', + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/farmatec.png`, + client_name: 'Farmatec', + }, + requests: [], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/index.ts b/agent/src/verifiers/index.ts new file mode 100644 index 0000000..e94b817 --- /dev/null +++ b/agent/src/verifiers/index.ts @@ -0,0 +1,86 @@ +import type { PlaygroundVerifierOptions } from '../verifier' +import { bundesregierungVerifier } from './bundesregierung' +import { cheapCarsVerifier } from './cheapCars' +import { deineBankVerifier } from './deineBank' +import { europeanUnionVerifier } from './europeanUnion' +import { farmatecVerifier } from './farmatec' +import { kvkVerifier } from './kvk' +import { openHorizonBankVerifier } from './openHorizonBank' +import { openIdInteropVerifier } from './openIdInteropApril' +import { pgeuVerifier } from './pgeu' +import { potentialVerifier } from './potentialUc4InteropApril' +import { redcarePharmacyVerifier } from './redcarePharmacy' +import type { TrustChain } from './trustChains' +import { trustPilotVerifier } from './trustPilot' +import { turboKeysVerifier } from './turboKeys' + +export const verifiers = [ + turboKeysVerifier, + kvkVerifier, + trustPilotVerifier, + openHorizonBankVerifier, + deineBankVerifier, + bundesregierungVerifier, + cheapCarsVerifier, + redcarePharmacyVerifier, + farmatecVerifier, + pgeuVerifier, + europeanUnionVerifier, + potentialVerifier, + openIdInteropVerifier, +] +export const allDefinitions = verifiers.flatMap((v): Array => v.requests) + +export const verifierTrustChains = [ + // --- Turbo keys --- + { + // Turbo keys is trusted by kvk + leaf: turboKeysVerifier.verifierId, + trustAnchor: kvkVerifier.verifierId, + }, + { + // Turbo keys is trusted by trust pilot + leaf: turboKeysVerifier.verifierId, + trustAnchor: trustPilotVerifier.verifierId, + }, + // --- Trust pilot --- + { + // Trust pilot is trusted by kvk + leaf: trustPilotVerifier.verifierId, + trustAnchor: kvkVerifier.verifierId, + }, + // --- Open horizon bank --- + { + // Open horizon bank is trusted by bundesregierung + leaf: openHorizonBankVerifier.verifierId, + trustAnchor: bundesregierungVerifier.verifierId, + }, + // --- Redcare pharmacy --- + { + // Redcare pharmacy is trusted by kvk + leaf: redcarePharmacyVerifier.verifierId, + trustAnchor: kvkVerifier.verifierId, + }, + { + // Redcare pharmacy is trusted by farmatec + leaf: redcarePharmacyVerifier.verifierId, + trustAnchor: farmatecVerifier.verifierId, + }, + { + // Redcare pharmacy is trusted by pgeu + leaf: redcarePharmacyVerifier.verifierId, + trustAnchor: pgeuVerifier.verifierId, + }, + // --- Pgeu --- + { + // Pgeu is trusted by european union + leaf: pgeuVerifier.verifierId, + trustAnchor: europeanUnionVerifier.verifierId, + }, + // --- Bundesregierung --- + { + // Bundesregierung is trusted by european union + leaf: bundesregierungVerifier.verifierId, + trustAnchor: europeanUnionVerifier.verifierId, + }, +] as const satisfies Array diff --git a/agent/src/verifiers/kvk.ts b/agent/src/verifiers/kvk.ts new file mode 100644 index 0000000..31cc5fb --- /dev/null +++ b/agent/src/verifiers/kvk.ts @@ -0,0 +1,11 @@ +import { AGENT_HOST } from '../constants' +import type { PlaygroundVerifierOptions } from '../verifier' + +export const kvkVerifier = { + verifierId: '0193687b-0c27-7b82-a686-ff857dc6bbb3', + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/kvk/verifier.png`, + client_name: 'KVK', + }, + requests: [], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/openHorizonBank.ts b/agent/src/verifiers/openHorizonBank.ts new file mode 100644 index 0000000..5668fab --- /dev/null +++ b/agent/src/verifiers/openHorizonBank.ts @@ -0,0 +1,39 @@ +import { AGENT_HOST } from '../constants' +import { certificateOfResidenceSdJwt } from '../issuers/koln' +import { healthIdSdJwt } from '../issuers/krankenkasse' +import { taxIdSdJwt } from '../issuers/steuern' +import type { PlaygroundVerifierOptions } from '../verifier' +import { pidSdJwtCredential } from './util' + +export const openHorizonBankVerifier = { + verifierId: '019368e8-54aa-788e-81c4-e60a59a09d87', + useCase: { + name: 'Open a bank account', + icon: 'bank', + tags: ['Federation support', 'Smart AI warnings', 'multi-credentials', 'mixed-credentials'], + }, + + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/openbank.png`, + client_name: 'Open Horizon Bank', + }, + requests: [ + { + name: 'PID and MDL (sd-jwt vc)', + purpose: + 'To open an Open Horizon Bank account, we need to verify your name, date of birth, country of residence and nationality', + credentials: [ + { format: 'dc+sd-jwt', vcts: [taxIdSdJwt.vct], fields: ['tax_number', 'affiliation_country'] }, + { format: 'dc+sd-jwt', vcts: [certificateOfResidenceSdJwt.vct], fields: ['resident_address', 'arrival_date'] }, + { + format: 'dc+sd-jwt', + vcts: [healthIdSdJwt.vct], + fields: ['health_insurance_id', 'affiliation_country', 'matching_institution_id'], + }, + pidSdJwtCredential({ + fields: ['given_name', 'family_name', 'birthdate', 'address.country', 'nationalities'], + }), + ], + }, + ], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/openIdInteropApril.ts b/agent/src/verifiers/openIdInteropApril.ts new file mode 100644 index 0000000..7280ea8 --- /dev/null +++ b/agent/src/verifiers/openIdInteropApril.ts @@ -0,0 +1,201 @@ +import { AGENT_HOST } from '../constants' +import { ageSdJwt } from '../issuers/credentials/ageSdJwt' +import { arfCompliantPidSdJwt } from '../issuers/credentials/arf18PidSdJwt' +import { mobileDriversLicenseMdoc } from '../issuers/credentials/mDLMdoc' +import { openIdSdJwt } from '../issuers/credentials/openIDSdJwt' +import { photoIdMdoc } from '../issuers/credentials/photoIdMdoc' + +import type { PlaygroundVerifierOptions } from '../verifier' +import { type MdocCredential, type SdJwtCredential, pidMdocCredential } from './util' + +const pidSdJwtVcNames = { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct], + fields: [ + // Mandatory + 'family_name', + 'given_name', + ], +} satisfies SdJwtCredential + +const pidSdJwtVcAge = { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct], + fields: [{ path: 'age_equal_or_over.18', values: [true] }], +} satisfies SdJwtCredential + +const pidSdJwtVcPostalCodeOrResidentCity = { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct], + fields: ['address.postal_code', 'address.locality', 'address.region'], + field_options: [['address.postal_code'], ['address.locality', 'address.region']], +} satisfies SdJwtCredential + +const pidSdJwtVcPostalCode = { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct], + fields: [{ path: 'address.postal_code', values: ['90210'] }], +} satisfies SdJwtCredential + +const ageSdJwtVcAge = { + format: 'dc+sd-jwt', + vcts: [ageSdJwt.vct], + fields: [{ path: 'age_over_18', values: [true] }], +} satisfies SdJwtCredential + +const openidSdJwtVcAge = { + format: 'dc+sd-jwt', + vcts: [openIdSdJwt.vct], + fields: [{ path: 'age_over_18', values: [true] }], +} satisfies SdJwtCredential + +const openidSdJwtVcNames = { + format: 'dc+sd-jwt', + vcts: [openIdSdJwt.vct], + fields: ['given_name', 'family_name'], +} satisfies SdJwtCredential + +const pidMdocAge = pidMdocCredential({ + fields: [{ path: 'age_over_18', values: [true] }], +}) + +const pidMdocNames = pidMdocCredential({ + fields: ['given_name', 'family_name'], +}) + +const pidMdocPostalCode = pidMdocCredential({ + fields: [{ path: 'resident_postal_code', values: ['90210'] }], +}) + +const pidMdocPostalCodeOrResidentCity = pidMdocCredential({ + fields: ['resident_postal_code', 'resident_city', 'resident_state'], + field_options: [['resident_postal_code'], ['resident_city', 'resident_state']], +}) + +const mdlNames = { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: ['given_name', 'family_name'], +} satisfies MdocCredential + +const mdlAge = { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: [{ path: 'age_over_18', values: [true] }], +} satisfies MdocCredential + +const mdlPostalCode = { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: [{ path: 'resident_postal_code', values: ['90210'] }], +} satisfies MdocCredential + +const photoIdAge = { + format: 'mso_mdoc', + doctype: photoIdMdoc.doctype, + namespace: 'org.iso.23220.1', + fields: [{ path: 'age_over_18', values: [true] }], +} satisfies MdocCredential + +const photoIdPostalCode = { + format: 'mso_mdoc', + doctype: photoIdMdoc.doctype, + namespace: 'org.iso.23220.1', + fields: [{ path: 'resident_postal_code', values: ['90210'] }], +} satisfies MdocCredential + +export const openIdInteropVerifier = { + verifierId: '8caaebcc-d48c-471b-86b0-a534e15c4774', + useCase: { + name: 'OpenID Foundation Interop Event April', + icon: 'interop', + tags: ['Digital Credentials API', 'OpenID4VP Draft 24', 'mDOC', 'SD-JWT VC'], + }, + + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/animo/verifier.png`, + client_name: 'Animo', + }, + + requests: [ + { + name: '#1 - mDL (mdoc) - Names', + purpose: '#1 - mDL (mdoc) - Names', + credentials: [mdlNames], + }, + { + name: '#2 - PID (sd-jwt-vc) - Names', + purpose: '#2 - PID (sd-jwt-vc) - Names', + credentials: [pidSdJwtVcNames], + }, + { + name: '#3 - PID (mdoc) - Names', + purpose: '#3 - PID (mdoc) - Names', + credentials: [pidMdocNames], + }, + { + name: '#4 - OpenID (sd-jwt-vc) - Names', + purpose: '#4 - OpenID (sd-jwt-vc) - Names', + credentials: [pidMdocNames], + }, + { + name: '#5 - Names - PID or mDL (mdoc)', + purpose: '#5 - Names - PID or mDL (mdoc)', + credentials: [pidMdocNames, mdlNames], + credential_sets: [[0, 1]], + }, + { + name: '#6 - Names - PID or OpenID (sd-jwt-vc)', + purpose: '#6 - Names - PID or OpenID (sd-jwt-vc)', + credentials: [pidSdJwtVcNames, openidSdJwtVcNames], + credential_sets: [[0, 1]], + }, + { + name: '#7 - PID - postal code or resident city (mdoc)', + purpose: '#7 - PID - postal code or resident city (mdoc)', + credentials: [pidMdocPostalCodeOrResidentCity], + }, + { + name: '#8 - PID - postal code or resident city (sd-jwt-vc)', + purpose: '#8 - PID - postal code or resident city (sd-jwt-vc)', + credentials: [pidSdJwtVcPostalCodeOrResidentCity], + }, + { + name: '#9 - Age over 18 - PID or mDL or PhotoID (mdoc)', + purpose: '#9 - Age over 18 - PID or mDL or PhotoID (mdoc)', + + credentials: [pidMdocAge, mdlAge, photoIdAge], + credential_sets: [[0, 1, 2]], + }, + { + name: '#10 - Postal code 90210 - PID or mDL or PhotoID (mdoc)', + purpose: '#10 - Postal code 90210 - PID or mDL or PhotoID (mdoc)', + + credentials: [pidMdocPostalCode, mdlPostalCode, photoIdPostalCode], + credential_sets: [[0, 1, 2]], + }, + { + name: '#11 - Age over 18 - PID or Age or OpenID (sd-jwt-vc)', + purpose: '#11 - Age over 18 - PID or Age or OpenID (sd-jwt-vc)', + + credentials: [pidSdJwtVcAge, ageSdJwtVcAge, openidSdJwtVcAge], + credential_sets: [[0, 1, 2]], + }, + { + name: '#12 - Postal code 90210 - PID (sd-jwt-vc)', + purpose: '#12 - Postal code 90210 - PID (sd-jwt-vc)', + + credentials: [pidSdJwtVcPostalCode], + }, + { + name: '#13 - Names - mDL (mdoc) or PID (sd-jwt-vc)', + purpose: '#13 - Names - mDL (mdoc) or PID (sd-jwt-vc)', + + credentials: [mdlNames, pidSdJwtVcNames], + credential_sets: [[0, 1]], + }, + ], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/pgeu.ts b/agent/src/verifiers/pgeu.ts new file mode 100644 index 0000000..9a6095e --- /dev/null +++ b/agent/src/verifiers/pgeu.ts @@ -0,0 +1,11 @@ +import { AGENT_HOST } from '../constants' +import type { PlaygroundVerifierOptions } from '../verifier' + +export const pgeuVerifier = { + verifierId: '01936903-8879-733f-8eaf-6f2fa862099c', + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/pgeu.png`, + client_name: 'The Pharmaceutical Group of the European Union', + }, + requests: [], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/potentialUc4InteropApril.ts b/agent/src/verifiers/potentialUc4InteropApril.ts new file mode 100644 index 0000000..83ebcc9 --- /dev/null +++ b/agent/src/verifiers/potentialUc4InteropApril.ts @@ -0,0 +1,154 @@ +import { AGENT_HOST } from '../constants' +import { arfCompliantPidSdJwt, arfCompliantPidUrnVctSdJwt, mobileDriversLicenseMdoc } from '../issuers/bdr' +import type { PlaygroundVerifierOptions } from '../verifier' +import { type MdocCredential, type SdJwtCredential, pidMdocCredential } from './util' + +const pidSdJwtVcNames = { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct, arfCompliantPidUrnVctSdJwt.vct], + fields: [ + // Mandatory + 'family_name', + 'given_name', + ], +} satisfies SdJwtCredential + +const pidSdJwtVcMandatory = { + format: 'dc+sd-jwt', + vcts: [arfCompliantPidSdJwt.vct, arfCompliantPidUrnVctSdJwt.vct], + fields: [ + // Mandatory + 'family_name', + 'given_name', + 'birth_date', + 'age_over_18', + + // Mandatory metadata + 'issuance_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + ], +} satisfies SdJwtCredential + +const pidMdocMandatory = pidMdocCredential({ + fields: [ + // Mandatory + 'family_name', + 'given_name', + 'birth_date', + 'age_over_18', + + // Mandatory metadata + 'issuance_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + ], +}) + +const pidMdocNames = pidMdocCredential({ + fields: ['family_name', 'given_name'], +}) + +const mDLMandatory = { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: [ + 'given_name', + 'family_name', + 'birth_date', + 'document_number', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'driving_privileges', + ], +} satisfies MdocCredential + +const mdlNames = { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: ['given_name', 'family_name'], +} satisfies MdocCredential + +export const potentialVerifier = { + verifierId: '826fc673-6c8b-4189-a5ec-0ed408f4e6a2', + useCase: { + name: 'Potential UC4 Interop Event April 2025', + icon: 'interop', + tags: ['ISO 18013-5', 'ISO 18013-7', 'PID SD-JWT VC', 'PID mDOC', 'Track 1', 'Track 2', 'Track 4'], + }, + + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/potential/verifier.png`, + client_name: 'Potential UC4 Interop Event', + }, + + requests: [ + // mDL (mdoc) + { + name: 'mDL (mdoc) - Names', + purpose: 'mDL - first_name and given_name', + credentials: [mdlNames], + }, + { + name: 'mDL (mdoc) - Mandatory', + purpose: 'mDL - all mandatory fields', + credentials: [mDLMandatory], + }, + + // mDL (mdoc) and PID (sd-jwt) + { + name: 'mDL (mdoc) - PID (sd-jwt-vc) - Mandatory', + purpose: 'mDL (mdoc) and PID (sd-jwt-vc) - all mandatory fields', + + credentials: [mDLMandatory, pidSdJwtVcMandatory], + }, + { + name: 'mDL (mdoc) - PID (sd-jwt-vc) - Names', + purpose: 'mDL (mdoc) and PID (sd-jwt-vc) - first_name and given_name', + + credentials: [mdlNames, pidSdJwtVcNames], + }, + + // mDL (mdoc) and PID (mdoc) + { + name: 'mDL (mdoc) - PID (mdoc) - Mandatory', + purpose: 'mDL (mdoc) and PID (mdoc) - all mandatory fields', + + credentials: [mDLMandatory, pidMdocMandatory], + }, + { + name: 'mDL (mdoc) - PID (mdoc) - Names', + purpose: 'mDL (mdoc) and PID (mdoc) - first_name and given_name', + + credentials: [mdlNames, pidMdocNames], + }, + + { + name: 'PID (mdoc) - Names', + purpose: 'PID (mdoc) - first_name and given_name', + credentials: [pidMdocNames], + }, + { + name: 'PID (mdoc) - Mandatory', + purpose: 'PID (mdoc) - all mandatory fields', + credentials: [pidMdocMandatory], + }, + + { + name: 'PID (sd-jwt-vc) - Names', + purpose: 'PID (sd-jwt-vc) - first_name and given_name', + credentials: [pidSdJwtVcNames], + }, + { + name: 'PID (sd-jwt-vc) - Mandatory', + purpose: 'PID (sd-jwt-vc) - all mandatory fields', + credentials: [pidSdJwtVcMandatory], + }, + ], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/redcarePharmacy.ts b/agent/src/verifiers/redcarePharmacy.ts new file mode 100644 index 0000000..f066542 --- /dev/null +++ b/agent/src/verifiers/redcarePharmacy.ts @@ -0,0 +1,45 @@ +import { AGENT_HOST } from '../constants' +import { healthIdSdJwt } from '../issuers/krankenkasse' +import type { PlaygroundVerifierOptions } from '../verifier' +import { pidSdJwtCredential } from './util' + +export const redcarePharmacyVerifier = { + verifierId: '01936901-2390-722e-b9f1-bf42db4db7ca', + useCase: { + name: 'Get an ePrescription', + icon: 'health', + tags: ['Federation support', 'QEAA', 'DCQL'], + }, + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/redcare.png`, + client_name: 'Redcare Pharmacy', + }, + + requests: [ + { + name: 'Receive your prescription (sd-jwt vc)', + purpose: 'To receive your prescription and finalize the transaction, we require the following attributes', + credentials: [ + { + format: 'dc+sd-jwt', + vcts: [healthIdSdJwt.vct], + fields: ['health_insurance_id', 'affiliation_country'], + }, + ], + }, + { + name: 'PID and Health-ID (sd-jwt vc)', + purpose: 'To give your medicine we need to verify your identity and prescription.', + credentials: [ + pidSdJwtCredential({ + fields: ['given_name', 'family_name', 'birthdate'], + }), + { + format: 'dc+sd-jwt', + vcts: [healthIdSdJwt.vct], + fields: ['health_insurance_id', 'wallet_e_prescription_code'], + }, + ], + }, + ], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/trustChains.ts b/agent/src/verifiers/trustChains.ts new file mode 100644 index 0000000..da9bd42 --- /dev/null +++ b/agent/src/verifiers/trustChains.ts @@ -0,0 +1,43 @@ +export type EntityId = string + +export type TrustChain = { + leaf: EntityId + intermediates?: Array + trustAnchor: EntityId +} + +export const flattenTrustChains = (trustChain: TrustChain) => { + return [trustChain.leaf, ...(trustChain.intermediates ?? []), trustChain.trustAnchor] +} + +export const isSubordinateTo = (trustChains: Array, issuer: EntityId, subject: EntityId) => { + // We only want to check one index so if the subject is directly under the issuer + // return chain.indexOf(issuer) + 1 === chain.indexOf(subject) + + const subjectVerifierId = subject.split('/').pop() + if (!subjectVerifierId) { + throw new Error('Subject verifier id not found') + } + + return trustChains + .map(flattenTrustChains) + .filter((chain) => chain.includes(issuer) && chain.includes(subjectVerifierId)) + .flatMap((chain) => { + const indexIssuer = chain.indexOf(issuer) + const indexSubject = chain.indexOf(subjectVerifierId) + + // TODO: Not sure if this is correct + return indexIssuer === indexSubject - 1 + }) +} + +export const getAuthorityHints = (trustChains: Array, entityId: EntityId) => { + return trustChains + .map(flattenTrustChains) + .filter((chain) => chain.includes(entityId)) + .flatMap((chain) => { + const index = chain.indexOf(entityId) + + return chain[index + 1] ?? [] + }) +} diff --git a/agent/src/verifiers/trustPilot.ts b/agent/src/verifiers/trustPilot.ts new file mode 100644 index 0000000..eb78713 --- /dev/null +++ b/agent/src/verifiers/trustPilot.ts @@ -0,0 +1,11 @@ +import { AGENT_HOST } from '../constants' +import type { PlaygroundVerifierOptions } from '../verifier' + +export const trustPilotVerifier = { + verifierId: '0193687f-20d8-720a-9139-ed939ba510fa', + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/trustpilot/verifier.webp`, + client_name: 'TrustPilot', + }, + requests: [], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/turboKeys.ts b/agent/src/verifiers/turboKeys.ts new file mode 100644 index 0000000..d580d12 --- /dev/null +++ b/agent/src/verifiers/turboKeys.ts @@ -0,0 +1,150 @@ +import { AGENT_HOST } from '../constants' +import { mobileDriversLicenseMdoc, mobileDriversLicenseSdJwt } from '../issuers/bdr' +import type { PlaygroundVerifierOptions } from '../verifier' +import { pidMdocCredential, pidSdJwtCredential } from './util' + +export const turboKeysVerifier = { + verifierId: 'c01ea0f3-34df-41d5-89d1-50ef3d181855', + useCase: { + name: 'Rent a car', + icon: 'car-rental', + tags: ['multi-credentials', 'mixed-credentials', 'Query languages', 'Federation support'], + }, + clientMetadata: { + logo_uri: `${AGENT_HOST}/assets/verifiers/turbokeys/verifier.png`, + client_name: 'TurboKeys', + }, + requests: [ + { + name: 'PID and MDL (sd-jwt vc)', + purpose: 'To secure your car reservations and finalize the transaction, we require the following attributes', + credentials: [ + { + vcts: [mobileDriversLicenseSdJwt.vct], + fields: [ + 'document_number', + 'portrait', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'driving_privileges', + ], + format: 'dc+sd-jwt', + }, + pidSdJwtCredential({ + fields: ['given_name', 'family_name', 'birthdate'], + }), + ], + }, + { + name: 'PID (sd-jwt vc) and MDL (mso_mdoc)', + purpose: 'To secure your car reservations and finalize the transaction, we require the following attributes', + credentials: [ + { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: [ + 'document_number', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'driving_privileges', + ], + }, + pidSdJwtCredential({ + fields: ['given_name', 'family_name', 'birthdate'], + }), + ], + }, + { + name: 'PID and MDL (both either sd-jwt vc or mso_mdoc, prefer sd-jwt vc)', + purpose: 'To secure your car reservations and finalize the transaction, we require the following attributes', + + credential_sets: [ + [1, 0], + [2, 3], + ], + credentials: [ + { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: [ + 'document_number', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'portrait', + 'driving_privileges', + ], + }, + { + format: 'dc+sd-jwt', + vcts: [mobileDriversLicenseSdJwt.vct], + fields: [ + 'document_number', + 'portrait', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'driving_privileges', + ], + }, + pidSdJwtCredential({ + fields: ['given_name', 'family_name', 'birthdate'], + }), + pidMdocCredential({ + fields: ['given_name', 'family_name', 'birth_date'], + }), + ], + }, + { + name: 'PID and MDL (both either sd-jwt vc or mso_mdoc, prefer mdoc)', + purpose: 'To secure your car reservations and finalize the transaction, we require the following attributes', + credential_sets: [ + [0, 1], + [3, 2], + ], + credentials: [ + { + format: 'mso_mdoc', + doctype: mobileDriversLicenseMdoc.doctype, + namespace: 'org.iso.18013.5.1', + fields: [ + 'document_number', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'portrait', + 'driving_privileges', + ], + }, + { + format: 'dc+sd-jwt', + vcts: [mobileDriversLicenseSdJwt.vct], + fields: [ + 'document_number', + 'portrait', + 'issue_date', + 'expiry_date', + 'issuing_country', + 'issuing_authority', + 'driving_privileges', + ], + }, + pidSdJwtCredential({ + fields: ['given_name', 'family_name', 'birthdate'], + }), + pidMdocCredential({ + fields: ['given_name', 'family_name', 'birth_date'], + }), + ], + }, + ], +} as const satisfies PlaygroundVerifierOptions diff --git a/agent/src/verifiers/util.ts b/agent/src/verifiers/util.ts new file mode 100644 index 0000000..62330d7 --- /dev/null +++ b/agent/src/verifiers/util.ts @@ -0,0 +1,192 @@ +import { randomUUID } from 'crypto' +import type { DcqlQuery, DifPresentationExchangeDefinitionV2 } from '@credo-ts/core' +import type { PlaygroundVerifierOptions } from '../verifier' + +export interface SdJwtCredential { + format: 'dc+sd-jwt' + vcts: string[] + issuers?: string[] + fields: Array }> + + // aka claim sets. Only used for DCQL + field_options?: string[][] +} + +export interface MdocCredential { + format: 'mso_mdoc' + doctype: string + namespace: string + fields: Array }> + + // aka claim sets. Only used for DCQL + field_options?: string[][] +} + +export function pidMdocCredential({ fields, field_options }: Pick) { + return { + format: 'mso_mdoc', + fields, + doctype: 'eu.europa.ec.eudi.pid.1', + namespace: 'eu.europa.ec.eudi.pid.1', + field_options, + } satisfies MdocCredential +} + +export function pidSdJwtCredential({ fields }: Pick) { + return { + format: 'dc+sd-jwt', + fields, + vcts: ['https://demo.pid-issuer.bundesdruckerei.de/credentials/pid/1.0'], + // issuers: [ + // 'https://demo.pid-issuer.bundesdruckerei.de/c', + // 'https://demo.pid-issuer.bundesdruckerei.de/c1', + // 'https://demo.pid-issuer.bundesdruckerei.de/b1', + // ], + } satisfies SdJwtCredential +} + +export function presentationDefinitionFromRequest( + request: PlaygroundVerifierOptions['requests'][number], + purpose?: string +): DifPresentationExchangeDefinitionV2 { + return { + id: randomUUID(), + name: request.name, + purpose: purpose ?? request.purpose, + input_descriptors: request.credentials.map((c, index) => ({ + id: c.format === 'mso_mdoc' ? c.doctype : randomUUID(), + group: request.credential_sets + ?.map((set, setIndex) => (set.includes(index) ? `${setIndex}` : undefined)) + .filter((s): s is string => s !== undefined), + format: + c.format === 'dc+sd-jwt' + ? { + 'vc+sd-jwt': { + 'sd-jwt_alg_values': ['ES256', 'ES384', 'EdDSA'], + 'kb-jwt_alg_values': ['ES256', 'ES384', 'EdDSA'], + }, + } + : { + mso_mdoc: { + alg: ['ES256', 'ES384', 'EdDSA'], + }, + }, + constraints: { + limit_disclosure: 'required', + fields: [ + ...c.fields.map((field) => + c.format === 'dc+sd-jwt' + ? { + path: [`$.${typeof field === 'string' ? field : field.path}`], + filter: + typeof field !== 'string' + ? { + enum: field.values, + } + : undefined, + } + : { + intent_to_retain: false, + path: [`$['${c.namespace}']['${typeof field === 'string' ? field : field.path}']`], + // Filter not allowed for mdoc + } + ), + ...(c.format === 'dc+sd-jwt' && c.issuers?.length + ? [ + { + path: ['$.iss'], + filter: { + type: 'string', + enum: c.issuers, + }, + }, + ] + : []), + ...(c.format === 'dc+sd-jwt' && c.vcts.length + ? [ + { + path: ['$.vct'], + filter: { + type: 'string', + ...(c.vcts.length === 1 ? { const: c.vcts[0] } : { enum: c.vcts }), + }, + }, + ] + : []), + ], + }, + })), + submission_requirements: request.credential_sets?.map((_, index) => ({ + rule: 'pick', + count: 1, + from: `${index}`, + })), + } +} + +export function dcqlQueryFromRequest( + request: PlaygroundVerifierOptions['requests'][number], + purpose?: string +): DcqlQuery { + return { + credentials: request.credentials.map((c, credentialIndex): DcqlQuery['credentials'][number] => + c.format === 'dc+sd-jwt' + ? { + id: `${credentialIndex}`, + format: c.format, + meta: { + vct_values: c.vcts, + }, + claims: [ + ...c.fields.map((f) => + typeof f === 'string' + ? { path: f.split('.'), id: f.replace('.', '_') } + : { path: f.path.split('.'), id: f.path.replace('.', '_'), values: f.values } + ), + ...(c.issuers?.length + ? [ + { + id: 'iss', + path: ['iss'], + values: c.issuers, + }, + ] + : []), + ], + claim_sets: c.field_options?.map((o) => { + const oo = o.map((oo) => oo.replace('.', '_')) + return c.issuers?.length ? [...oo, 'iss'] : oo + }), + } + : { + id: `${credentialIndex}`, + format: c.format, + meta: { + doctype_value: c.doctype, + }, + claims: c.fields.map((f) => + typeof f === 'string' + ? { id: f.replace('.', '_'), path: [c.namespace, f], intent_to_retain: false } + : { + id: f.path.replace('.', '_'), + path: [c.namespace, f.path], + intent_to_retain: false, + values: f.values, + } + ), + claim_sets: c.field_options?.map((o) => o.map((oo) => oo.replace('.', '_'))), + } + ), + credential_sets: request.credential_sets + ? request.credential_sets.map((set) => ({ + options: set.map((v) => [`${v}`]), + purpose: purpose ?? request.purpose, + })) + : [ + { + options: [request.credentials.map((_, index) => `${index}`)], + purpose: purpose ?? request.purpose, + }, + ], + } +} diff --git a/agent/tsconfig.json b/agent/tsconfig.json index 11073e3..62dc28d 100644 --- a/agent/tsconfig.json +++ b/agent/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "target": "ES2020", "outDir": "dist", - "module": "commonjs", + "module": "NodeNext", + "moduleResolution": "nodenext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, diff --git a/app/app/globals.css b/app/app/globals.css index 1b4ca7b..f48e41a 100644 --- a/app/app/globals.css +++ b/app/app/globals.css @@ -2,18 +2,6 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} +/* * { + scrollbar-gutter: stable; +} */ diff --git a/app/app/invitation/page.tsx b/app/app/invitation/page.tsx new file mode 100644 index 0000000..1d152ad --- /dev/null +++ b/app/app/invitation/page.tsx @@ -0,0 +1,31 @@ +'use client' + +import { useEffect, useState } from 'react' + +export default function InvitationPage() { + const [iFrameUrl, setIFrameUrl] = useState() + + useEffect( + () => + setIFrameUrl( + location.href + .replace('https://funke.animo.id/invitation', 'https://paradym.id/invitation') + .replace('http://localhost:3000/invitation', 'https://paradym.id/invitation') + ), + [] + ) + + return ( +