diff --git a/.env.example b/.env.example index d65229c..5b28a34 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,8 @@ ENABLE_HTTPS_FOR_DEV=false # ONLY for development when need https; default ENABLE_ACCESS_LOGGING=true ENABLE_STATUS_SERVICE=false -STATUS_SERVICE_ENDPOINT=localhost:4008 -SIGNING_SERVICE_ENDPOINT=localhost:4006 +STATUS_SERVICE=localhost:4008 +SIGNING_SERVICE=localhost:4006 # Tokens for protecting tenant endpoints. # Add a token for any tenant name, diff --git a/.gitignore b/.gitignore index 954175e..31c4c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -75,7 +75,8 @@ types/ # dotenv environment variables file .env .signing-service.env -.status-service.env +.status-service-db.env +.status-service-git.env .coordinator.env .env.test diff --git a/.status-service-db.env b/.status-service-db.env new file mode 100644 index 0000000..de5b403 --- /dev/null +++ b/.status-service-db.env @@ -0,0 +1,18 @@ +PORT=4008 # default port is 4008 +ENABLE_HTTPS_FOR_DEV=false # ONLY for dev when need https; default is false + +# Database specific environment variables +CRED_STATUS_SERVICE=mongodb +CRED_STATUS_DID_SEED=z1AackbUm8U69ohKnihoRRFkXcXJd4Ra1PkAboQ2ZRy1ngB +STATUS_CRED_SITE_ORIGIN=https://credentials.example.edu +CRED_STATUS_DB_URL=mongodb+srv://user:pass@domain.mongodb.net?retryWrites=false +CRED_STATUS_DB_HOST=domain.mongodb.net # ignored if CRED_STATUS_DB_URL is configured +CRED_STATUS_DB_PORT=27017 # ignored if CRED_STATUS_DB_URL is configured +CRED_STATUS_DB_USER=testuser # ignored if CRED_STATUS_DB_URL is configured +CRED_STATUS_DB_PASS=testpass # ignored if CRED_STATUS_DB_URL is configured +CRED_STATUS_DB_NAME= # autogenerated if omitted +STATUS_CRED_TABLE_NAME= # autogenerated if omitted +USER_CRED_TABLE_NAME= # autogenerated if omitted +CONFIG_TABLE_NAME= # autogenerated if omitted +EVENT_TABLE_NAME= # autogenerated if omitted +CRED_EVENT_TABLE_NAME= # autogenerated if omitted diff --git a/.status-service-git.env b/.status-service-git.env new file mode 100644 index 0000000..fdcccfc --- /dev/null +++ b/.status-service-git.env @@ -0,0 +1,12 @@ +PORT=4008 # default port is 4008 +ENABLE_HTTPS_FOR_DEV=false # ONLY for dev when need https; default is false + +# Git specific environment variables +CRED_STATUS_SERVICE=github +CRED_STATUS_DID_SEED=z1AackbUm8U69ohKnihoRRFkXcXJd4Ra1PkAboQ2ZRy1ngB +CRED_STATUS_REPO_OWNER=digitalcredentials +CRED_STATUS_REPO_NAME=credential-status-test-jc +CRED_STATUS_REPO_ID=12345678 # only required when CRED_STATUS_SERVICE = 'gitlab' +CRED_STATUS_META_REPO_NAME=credential-status-metadata-test-jc +CRED_STATUS_META_REPO_ID=87654321 # only required when CRED_STATUS_SERVICE = 'gitlab' +CRED_STATUS_ACCESS_TOKEN=REPLACE_THIS_WITH_A_GITHUB_ACCESS_TOKEN diff --git a/.status-service.env b/.status-service.env deleted file mode 100644 index 4edc38a..0000000 --- a/.status-service.env +++ /dev/null @@ -1,9 +0,0 @@ -PORT=4008 #default port is 4008 -ENABLE_HTTPS_FOR_DEV=false # ONLY for dev when need https; default is false - -# the CRED_STATUS_* values are used to instantiate the status list manager -CRED_STATUS_REPO_OWNER=jchartrand -CRED_STATUS_REPO_NAME=dcc-status-test -CRED_STATUS_META_REPO_NAME=dcc-status-meta-test -CRED_STATUS_ACCESS_TOKEN=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 -CRED_STATUS_DID_SEED=z1AackbUm8U69ohKnihoRRFkXcXJd4Ra1PkAboQ2ZRy1ngB diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d1c414..2d9db79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ -# exchange-coordinator Changelog +# workflow-coordinator Changelog ## 1.0.0 - TBD ### Added - - Initial commit. + +### Changed +- Convert Status List 2021 to Bitstring Status List. +- Differentiate between database status service and Git status service. +- Rename environment variables. +- Update revocation and suspension instructions. diff --git a/README.md b/README.md index 2bbce0c..e8d9f92 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ [![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/workflow-coordinator/main.yml?branch=main)](https://github.com/digitalcredentials/workflow-coordinator/actions?query=workflow%3A%22Node.js+CI%22) -A NodeJS Express server that coordinates micro-services within a Docker Compose Network to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) to a wallet like the [Learner Credential Wallet (LCW)](https://lcw.app) using the [exchange protocol of the VC-API spec](https://w3c-ccg.github.io/vc-api/#initiate-exchange) and either the [Credential Handler API (CHAPI)](https://chapi.io) or the custom DCC deeplink protocol to select a wallet. +An Express server that coordinates micro-services within a Docker Compose Network to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/) to a wallet like the [Learner Credential Wallet (LCW)](https://lcw.app) using the [exchange protocol of the VC-API spec](https://w3c-ccg.github.io/vc-api/#initiate-exchange) and either the [Credential Handler API (CHAPI)](https://chapi.io) or the custom DCC deep link protocol to select a wallet. This is meant to be used within a larger institutional system that already handles authentication and storage/retrieval of the user data (needed for the credential), and so simply passes that data to this system after authentication, at which point this system then largely handles the exchange with the wallet. -NOTE: because this coordinator interacts with a wallet through the [exchange protocol of the VC-API spec](https://w3c-ccg.github.io/vc-api/#initiate-exchange), the coordinator has to be callable from the wallet, and for wallets that run on a phone, this can be tricky when trying this out locally. If you are new to this, you may want to first start by experimenting with the [DCC Issuer Coordinator](https://github.com/digitalcredentials/issuer-coordinator) which issuers credentials that can then be independently imported into a wallet. +NOTE: because this coordinator interacts with a wallet through the [exchange protocol of the VC-API spec](https://w3c-ccg.github.io/vc-api/#initiate-exchange), the coordinator has to be callable from the wallet, and for wallets that run on a phone, this can be tricky when trying this out locally. If you are new to this, you may want to first start by experimenting with the [DCC Issuer Coordinator](https://github.com/digitalcredentials/issuer-coordinator) which issues credentials that can then be independently imported into a wallet. We have also made available a public demonstration of the exchange, which you can try by opening this [link](https://issuer.dcconsortium.org/demo) from a web browser on the same phone on which you've installed the [Learner Credential Wallet (LCW)](https://lcw.app) @@ -16,22 +16,26 @@ We have also made available a public demonstration of the exchange, which you ca - [Architecture](#architecture) - [Wallet Exchange](#wallet-exchange) - [API](#api) -- [Easy Start](#easy-start) - - [Learner Credential Wallet](#learner-credential-wallet) +- [Quick Start](#quick-start) + - [Install Docker](#install-docker) + - [Create Docker Compose File](#create-docker-compose-file) + - [Run Service](#run-service) + - [Issue Credentials](#issue-credentials) - [Versioning](#versioning) - [Configuration](#configuration) - - [Environment Variables](#environment-variables) + - [Generate New Key](#generate-new-key) - [Tenants](#tenants) - [Signing Key](#signing-key) - [DID Registries](#did-registries) - [did:key](#did-key) - [did:web](#did-web) - [Protecting Tenant Endpoints](#protecting-tenant-endpoints) - - [Revocation](#revocation) + - [Revocation and Suspension](#revocation-and-suspension) + - [Environment Variables](#environment-variables) - [Usage](#usage) - [Integration](#integration) - [Issuing](#issuing) - - [Revoking](#revoking) + - [Revoking and Suspending](#revoking-and-suspending) - [Development](#development) - [Testing](#testing) - [Contribute](#contribute) @@ -39,7 +43,7 @@ We have also made available a public demonstration of the exchange, which you ca ## Summary -Use this server to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) to a wallet like the [Learner Credential Wallet (LCW)](https://lcw.app). Credentials can optionally be allocated a [revocation status](https://www.w3.org/TR/vc-status-list/) that can later be used to revoke the credential. +Use this server to issue [Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/) to a wallet like the [Learner Credential Wallet (LCW)](https://lcw.app). Credentials can optionally be allocated a [revocation status](https://www.w3.org/TR/vc-status-list/) that can later be used to revoke the credential. The issued credentials are _assigned_ to a [Decentralized Identifier (DID)](https://www.w3.org/TR/did-core/) that the wallet provides (on behalf of the holder) to the issuer as part of the exchange. DIDs are effectively collections of cryptographic key pairs, which in this case later allow the holder to demonstrate that they control the credential by signing challenges using a private key associated with their DID. @@ -47,32 +51,32 @@ The issued credentials are _assigned_ to a [Decentralized Identifier (DID)](http This is an express app intended to run as a service within a Docker Compose network. This app coordinates calls to other express apps running as services within the same Docker Compose network, in particular: -* [DCC transaction-manager-service](https://github.com/digitalcredentials/transaction-manager-service) +* [DCC transaction-service](https://github.com/digitalcredentials/transaction-service) * [DCC signing-service](https://github.com/digitalcredentials/signing-service) and optionally also: -* [DCC credential-status-manager-git](https://github.com/digitalcredentials/credential-status-manager-git) -* [DCC template-service](https://github.com/digitalcredentials/template-service) (IN PROGRESS) +* DCC [database credential status service](https://github.com/digitalcredentials/status-service-db) or [Git credential status service](https://github.com/digitalcredentials/status-service-git) +* [DCC template service](https://github.com/digitalcredentials/template-service) (IN PROGRESS) Note that all the calls to the internal services are only available within the Docker Compose network, and are not exposed externally. -Typical use would be to run this in combination with something like nginx-proxy and acme-companion (for the automated creation, renewal and use of SSL certificates) using docker-compose. You may also run your own apps within the same Docker Compose network. You might, for example, run a react app with a user interface from which the student can request the credential. [Usage - Integration](#integration) further discusses how to incorporate this system into your own institutional system. +Typical use would be to run this in combination with something like `nginx-proxy` and `acme-companion` (for the automated creation, renewal and use of SSL certificates) using `docker-compose`. You may also run your own apps within the same Docker Compose network. You might, for example, run a react app with a user interface from which the student can request the credential. [Usage - Integration](#integration) further discusses how to incorporate this system into your own institutional system. ## Wallet Exchange This issuer implements the [VC-API Exchange protocol](https://w3c-ccg.github.io/vc-api/) which in this case is essentially: -1. the wallet (controlled by the recipient/holder, e.g., a student) tells the issuer that it wants to start a specific exchange (in this case to get a specific credential, like a diploma) -2. the issuer replies, saying that the wallet must first provide a [Decentralized Identifier (DID)](https://www.w3.org/TR/did-core/) that belongs to the holder (i.e, the student), along with signed proof that the DID does in fact belong to the holder. -3. the wallet sends back the DID and proof (in a DIDAuth Verifiable Presentation) -4. the issuer verifies the proof and replies with the requested credential, issued to the supplied DID +1. Wallet (controlled by the recipient/holder, e.g., a student) tells the issuer that it wants to start a specific exchange (in this case to get a specific credential, like a diploma) +2. Issuer replies, saying that the wallet must first provide a [Decentralized Identifier (DID)](https://www.w3.org/TR/did-core/) that belongs to the holder (i.e, the student), along with signed proof that the DID does in fact belong to the holder +3. Wallet sends back the DID and proof (in a DIDAuth Verifiable Presentation) +4. Issuer verifies the proof and replies with the requested credential, issued to the supplied DID NOTE: Issuing the credential to the holder's DID later allows the holder to prove that they _control_ the credential by using their DID to sign challenges from verifiers. -NOTE: we also provide an option to skip the first two steps so that the wallet can immediately post the DIDAuth after the deeplink opens it. This provides backwards compatability with wallets that implemented this simpler flow (for example as part of JFF PlugFest 2). +NOTE: we also provide an option to skip the first two steps so that the wallet can immediately post the DIDAuth after the deep link opens it. This provides backwards compatability with wallets that implemented this simpler flow (for example as part of JFF PlugFest 2). -NOTE: We further provide a convenience endpoint that is not part of the VC-API spec and that effectively allows the institution to delegate most handling to an exchange coordinator by giving the exchange coordinator everything it needs to manage the exchange without further help from the institution. This is not part of the VC-API spec and is only meant to make implementation easier for the institution. This endpoint is called by institutional software at the point after the student has been authenticated by the institutianl authentication system and the data for the credential has been retrieved from the institutional data store. +NOTE: We further provide a convenience endpoint that is not part of the VC-API spec and that effectively allows the institution to delegate most handling to a workflow coordinator by giving the workflow coordinator everything it needs to manage the exchange without further help from the institution. This is not part of the VC-API spec and is only meant to make implementation easier for the institution. This endpoint is called by institutional software at the point after the student has been authenticated by the institutianl authentication system and the data for the credential has been retrieved from the institutional data store. The flow looks like: @@ -82,26 +86,25 @@ TODO: ADD DIAGRAM This system implements four public endpoints: -Three implement the [VC-API](https://w3c-ccg.github.io/vc-api/) spec: +Three implement from the [VC-API](https://w3c-ccg.github.io/vc-api/) spec: * [POST /exchange/{exchangeID}](https://w3c-ccg.github.io/vc-api/#initiate-exchange) - * [POST /exchange/{exchangeID}/{transactionID}](https://w3c-ccg.github.io/vc-api/#initiate-exchange) + * [POST /exchange/{exchangeID}/{transactionID}](https://w3c-ccg.github.io/vc-api/#continue-exchange) * [POST /credentials/status](https://w3c-ccg.github.io/vc-api/#update-status) -And a fourth convenience endpoint that handles most of the exchange on behalf of the institution: +...and a fourth convenience endpoint that handles most of the exchange on behalf of the institution: - * POST /exchange/setup + * `POST /exchange/setup` -With this endpoint the institution posts a list of the unsigned verifiable credentials for which it wants deeplinks or a [Verifiable Presentation Request (VPR)](https://w3c-ccg.github.io/vp-request-spec/) that can be used with the [Credential Handler API (CHAPI)](https://chapi.io). The posted object should have the following structure: +With this endpoint the institution posts a list of the unsigned verifiable credentials for which it wants deep links or a [Verifiable Presentation Request (VPR)](https://w3c-ccg.github.io/vp-request-spec/) that can be used with the [Credential Handler API (CHAPI)](https://chapi.io). The posted object should have the following structure: -``` +```js { - "tenantName": "someTenantName", - "data": [ - {"vc": someVCGoesHere, "retrievalId": anIdWithWhichToIdentifyThisVCInTheResult}, - {"vc": anotherVCGoesHere, "retrievalId": aDifferentRetrievalId}, - ... however many more vcs you want to post - ] + "tenantName": "someTenantName", + "data": [ + {"vc": {/*someVCGoesHere*/}, "retrievalId": "anIdWithWhichToIdentifyThisVCInTheResult"}, + {"vc": {/*anotherVCGoesHere*/}, "retrievalId": "aDifferentRetrievalId"} + ] } ``` @@ -111,65 +114,58 @@ For example, this is the data you'd post for a single credential to an unprotect ```json { - "tenantName": "UN_PROTECTED_TEST", - "data": [ - { - "retrievalId": "anyIdThatIsMeaningfulForYou", - "vc": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" - ], - "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", - "type": [ - "VerifiableCredential", - "OpenBadgeCredential" - ], - "name": "DCC Test Credential", - "issuer": { - "type": [ - "Profile" - ], - "id": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC", - "name": "Digital Credentials Consortium Test Issuer", - "url": "https://dcconsortium.org", - "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" - }, - "issuanceDate": "2023-08-02T17:43:32.903Z", - "credentialSubject": { - "type": [ - "AchievementSubject" - ], - "achievement": { - "id": "urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922", - "type": [ - "Achievement" - ], - "achievementType": "Diploma", - "name": "Badge", - "description": "This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.", - "criteria": { - "type": "Criteria", - "narrative": "This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**." - }, - "image": { - "id": "https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png", - "type": "Image" - } - }, - "name": "Jane Doe" - } + "tenantName": "UN_PROTECTED_TEST", + "data": [ + { + "retrievalId": "anyIdThatIsMeaningfulForYou", + "vc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": { + "type": ["Profile"], + "id": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC", + "name": "Digital Credentials Consortium Test Issuer", + "url": "https://dcconsortium.org", + "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" + }, + "issuanceDate": "2023-08-02T17:43:32.903Z", + "credentialSubject": { + "type": ["AchievementSubject"], + "achievement": { + "id": "urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922", + "type": ["Achievement"], + "achievementType": "Diploma", + "name": "Badge", + "description": "This is a sample credential issued by the Digital Credentials Consortium to demonstrate the functionality of Verifiable Credentials for wallets and verifiers.", + "criteria": { + "type": "Criteria", + "narrative": "This credential was issued to a student that demonstrated proficiency in the Python programming language that occurred from **February 17, 2023** to **June 12, 2023**." + }, + "image": { + "id": "https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png", + "type": "Image" } + }, + "name": "Jane Doe" } - ] + } + } + ] } ``` +The endpoint returns a JSON object that provides three options for selecting a wallet: -The endpoint returns a json object that provides three options for selecting a wallet: - - * [a _direct_ custom DCC deeplink](#deeplink) - * [a _vpr_ custom DCC deeplink](#deeplink) + * [a _direct_ custom DCC deep link](#deeplink) + * [a _vpr_ custom DCC deep link](#deeplink) * a [Verifiable Presentation Request (VPR)](https://w3c-ccg.github.io/vp-request-spec/) An example of the returned object: @@ -195,20 +191,19 @@ An example of the returned object: "domain": "http://localhost:4005" } }] - ``` -The **retrievalId** is used to identify the result for each credential when more than one credential has been posted in the same post. The issuer supplies these retrievalIds when posting the data. The retrievalId can be anything that makes sense for the issuer. +The **retrievalId** is used to identify the result for each credential when more than one credential has been posted in the same post. The issuer supplies these IDs when posting the data. The `retrievalId` can be anything that makes sense for the issuer. -The **directDeepLink** will open the [Learner Credential Wallet](https://lcw.app) after which the wallet invokes the **vc_request_url** that is passed as a query parameter on the deeplink, and gets the signed VC back immediately. +The **directDeepLink** will open the [Learner Credential Wallet](https://lcw.app) after which the wallet invokes the **vc_request_url** that is passed as a query parameter on the deep link, and gets the signed VC back immediately. -The **vprDeepLink** will open the [Learner Credential Wallet](https://lcw.app) after which the wallet similarly invokes the **vc_request_url** but in this case the result of that call is a [Verifiable Presentation Request (VPR)](https://w3c-ccg.github.io/vp-request-spec/) request for a DIDAuth. So this is a two step exchange whereas the directDeepLink is a one step exchange. **IMPORTANT NOTE**: this flow has not yet been implemented in the [Learner Credential Wallet](https://lcw.app). +The **vprDeepLink** will open the [Learner Credential Wallet](https://lcw.app) after which the wallet similarly invokes the **vc_request_url** but in this case the result of that call is a [Verifiable Presentation Request (VPR)](https://w3c-ccg.github.io/vp-request-spec/) request for a DIDAuth. So this is a two-step exchange whereas the `directDeepLink` is a one-step exchange. -The **chapiVPR** is a [Verifiable Presentation Request (VPR)](https://w3c-ccg.github.io/vp-request-spec/) that is meant to be used with a [Credential Handler API (CHAPI)](https://chapi.io) _get_ call to pass the [VPR](https://w3c-ccg.github.io/vp-request-spec/) into a wallet which will then make a call to the exchange endpoint to get back the signed verifiable credential. **IMPORTANT NOTE**: this flow has not yet been implemented in the [Learner Credential Wallet](https://lcw.app). +The **chapiVPR** is a [Verifiable Presentation Request (VPR)](https://w3c-ccg.github.io/vp-request-spec/) that is meant to be used with a [Credential Handler API (CHAPI)](https://chapi.io) _get_ call to pass the [VPR](https://w3c-ccg.github.io/vp-request-spec/) into a wallet which will then make a call to the exchange endpoint to get back the signed verifiable credential. -The institutional software then offers the student the appropriate option. For an example of how this would work look at the GET /demo endpoint which demonstrates with a working example how an institution would use the exchange-coordinator. +The institutional software then offers the student the appropriate option. For an example of how this would work look at the `GET /demo` endpoint which demonstrates with a working example how an institution would use the `workflow-coordinator`. -## Easy Start +## Quick Start We've tried hard to make this as simple as possible to install and maintain, but also easy to evaluate and understand as you consider whether digital credentials are useful for your project, and whether this issuer would work for you. @@ -218,32 +213,39 @@ Installing and running the issuer is straightforward and should take less than f Docker have made this very easy, with [installers for Windows, Mac, and Linux](https://docs.docker.com/engine/install/) that make it as easy to install as any other application. -### Make a Docker Compose file +### Create Docker Compose File -Create a file called docker-compose.yml and add the following +Create a file called `docker-compose.yml` and add the following: -``` +```yaml version: '3.5' services: - exchange-coordinator: + coordinator: image: digitalcredentials/workflow-coordinator:0.1.0 ports: - "4005:4005" - signing-service: + signing: image: digitalcredentials/signing-service:0.1.0 - transaction-service: + transaction: image: digitalcredentials/transaction-service:0.1.0 + status: + image: digitalcredentials/status-service-db:0.1.0 + # NOTE: If you would prefer to use the Git based status manager instead + # of the database status manager, uncomment this section and comment + # out the previous section + # status: + # image: digitalcredentials/status-service-git:0.1.0 ``` Note that as of this writing (October 2nd 2023), the versions of each image are at 0.1.0. These will change over time. Read more in [Versioning](#versioning). -### Run it +### Run Service From the terminal in the same directory that contains your docker-compose.yml file: ```docker compose up``` -### Issue +### Issue Credentials This is a bit tricky because the credentials are issued to a wallet like the [Learner Credential Wallet (LCW)](https://lcw.app) and so your wallet needs to make a call to your locally running issuer, which normally runs on localhost, and which your phone doesn't usually by default have access to. You have at least two choices here: @@ -267,7 +269,7 @@ NOTE: Revocation is not enabled in the Quick Start. You've got to setup a couple ## Versioning -The workflow-coordinator and the services it coordinates are all intended to run as docker images within a docker compose network. For convenience we've published those images to Docker Hub so that you don't have to build them locally yourself from the github repositories. +The `workflow-coordinator` and the services it coordinates are all intended to run as docker images within a docker compose network. For convenience we've published those images to Docker Hub so that you don't have to build them locally yourself from the github repositories. The images on Docker Hub will of course be updated to add new functionality and fix bugs. Rather than overwrite the default (`latest`) version on Docker Hub for each update, we've adopted the [Semantic Versioning Guidelines](https://semver.org) with our docker image tags. @@ -279,68 +281,115 @@ If you do ever want to work from the source code in the repository and build you ## Configuration -There are a few things you'll want to configure, in particular setting your own signing keys (so that only you can sign your credentials). Other options include enabling revocation, or allowing for 'multi-tenant' signing, which you might use, for example, to sign credentials for different courses with a different key. +There are a few things you'll want to configure. These include, but may not be limited to: +* Your signing keys, which enable only you to sign your credentials +* Revocation/suspension support +* "Multi-tenant" signing, which enables you to use different keys for different credentialing purposes (e.g., signing credentials for different courses) -The app is configured with three .env files: +The app is configured with three `.env` files (Note that you only need to configure one of the `.status-service-*.env` files, depending on if you are using the database status manager or the Git status manager): -* [.coordinator.env](./.coordinator.env) -* [.signing-service.env](./.signer.env) -* [.status-service.env](./.signer.env) +* [.coordinator.env](.coordinator.env) +* [.signing-service.env](.signing-service.env) +* [.status-service-db.env](.status-service-db.env) OR [.status-service-git.env](.status-service-git.env) -You can simply uncomment the lines in this [docker-compose.yml](./docker-compose.yml) to use the default .env files that are included in this repo. You'll of course have to modify the contents of the .env files as described in this README. +If you've used the Quick Start `docker-compose.yml`, then you'll have to change it a bit to point at these files. Alternatively, we've pre-configured this [docker-compose.yml](docker-compose.yml), though, so you can just use that. -### Change Default Signing key +The issuer is pre-configured with a default signing key for testing that can only be used for testing and evaluation. Any credentials signed with this key are meaningless because anyone else can use it to sign credentials, and so could create fake copies of your credentials which would appear to be properly signed. There would be no way to know that it was fake. So, you'll want to add our own key which you do by generating a new key and setting it for a new tenant name. -The issuer is pre-configured with a default signing key that can only be used for testing and evaluation. Any credentials signed with this key are meaningless because anyone else can use it to sign credentials, and so could create fake copies of your credentials which would appear to be properly signed. There would be no way to know that it was fake. +#### Generate New Key -TODO: may want to by default auto-generate an ephemeral key on startup, that only lasts for the life of the process. Upside is that it wouldn't validate in any registry and so would force people to generate and set their own key. Downside is that credentials wouldn't validate, and so wouldn't demonstrate that functionality, but on the hand, would demonstrate failed verification. +To issue your own credentials, you must generate your own signing key and keep it private. At the moment, the issuer supports two [DID](https://www.w3.org/TR/did-core/) key formats/protocols: `did:key` and `did:web`. -#### Generate a new key +The `did:key` DID is one of the simpler DID implementations and doesn't require that the DID document be hosted anywhere. However, many organizations are likely to prefer the `did:web` DID for production deployments. This DID format and protocol allows the owner to rotate (change) their signing key without having to update every credential that is signed by the old keys. -To issue your own credentials you must generate your own signing key and keep it private. We've tried to make that a little easier by providing a convenience endpoint in the issuer that you can use to generate a brand new key. You can hit the endpoint with the following CURL command (in a terminal): +We've tried to simplify key generation by providing convenience endpoints in the issuer that you can use to generate a brand new key. You can generate a DID key with these cURL commands (in a terminal): -`curl --location 'http://localhost:4007/seedgen'` +- `did:key`: + ```bash + curl --location 'http://localhost:4005/did-key-generator' + ``` +- `did:web`: + ```bash + curl \ + --location 'localhost:4006/did-web-generator' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "url": "https://raw.githubusercontent.com/user-or-org/did-web-test/main" + }' + ``` -This will return a json document with: +These commands will return a JSON document that contains the following data: -- a seed +- a secret seed - the corresponding DID -- the corresponding DID Document +- the corresponding DID document -The returned result will look something like this: +Here is an example output for `did:key`: +```json +{ + "seed": "z1AjQUBZCNoiyPUC8zbbF29gLdZtHRqT6yPdFGtqJa5VfQ6", + "did": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", + "verificationMethod": [{ + "id": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", + "type": "Ed25519VerificationKey2020", + "controller": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", + "publicKeyMultibase": "z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4" + }], + "authentication": ["did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4"], + "assertionMethod": ["did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4"], + "capabilityDelegation": ["did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4"], + "capabilityInvocation": ["did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4"], + "keyAgreement": [{ + "id": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6LSnYW9e4Q4EXTvdjDhKyr2D1ghBfSLa5dJGBfzjG6hyPEt", + "type": "X25519KeyAgreementKey2020", + "controller": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", + "publicKeyMultibase": "z6LSnYW9e4Q4EXTvdjDhKyr2D1ghBfSLa5dJGBfzjG6hyPEt" + }] + } +} ``` + +...and here is an example output for `did:web` \*: + +```json { - "seed": "z1AjQUBZCNoiyPUC8zbbF29gLdZtHRqT6yPdFGtqJa5VfQ6", - "did": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", - "didDocument": { - "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/security/suites/x25519-2020/v1"], - "id": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", - "verificationMethod": [{ - "id": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", - "type": "Ed25519VerificationKey2020", - "controller": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", - "publicKeyMultibase": "z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4" - }], - "authentication": ["did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4"], - "assertionMethod": ["did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4"], - "capabilityDelegation": ["did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4"], - "capabilityInvocation": ["did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4"], - "keyAgreement": [{ - "id": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4#z6LSnYW9e4Q4EXTvdjDhKyr2D1ghBfSLa5dJGBfzjG6hyPEt", - "type": "X25519KeyAgreementKey2020", - "controller": "did:key:z6MkweTn1XVAiFfHjiH48oLknjNqRs43ayzguc8G8VbEAVm4", - "publicKeyMultibase": "z6LSnYW9e4Q4EXTvdjDhKyr2D1ghBfSLa5dJGBfzjG6hyPEt" - }] - } + "seed": "z1AcNXDnko1P6QMiZ3bxsraNvVtRbpXKeE8GNLDXjBJ5UHz", + "decodedSeed": "DecodedUint8ArraySeed", + "did": "did:web:raw.githubusercontent.com:user-or-org:did-web-test:main", + "didDocument": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:web:raw.githubusercontent.com:user-or-org:did-web-test:main", + "assertionMethod": [ + { + "id": "did:web:raw.githubusercontent.com:user-or-org:did-web-test:main#z6MkfGZKFTyxiH9HgFUHbPQigEWh8PtFaRkESt9oQLiTvhVq", + "type": "Ed25519VerificationKey2020", + "controller": "did:web:raw.githubusercontent.com:user-or-org:did-web-test:main", + "publicKeyMultibase": "z6MkfGZKFTyxiH9HgFUHbPQigEWh8PtFaRkESt9oQLiTvhVq" + } + ] + } } ``` -Now that you've got your key you'll want to set it... +**\* Note:** For the `did:web` key, the value of `didDocument` needs to be hosted at `${DID_WEB_URL}/.well-known/did.json`, where `DID_WEB_URL` is the issuer controlled URL that was passed as the `url` field of the request body in the `did:web` cURL command above. In the example above, this URL is https://raw.githubusercontent.com/user-or-org/did-web-test/main, because we are using GitHub to host a DID document in a repo named `did-web-test`, owned by user/org `user-or-org`, at the path `/.well-known/did.json`. In a production deployment, this might be something like https://registrar.example.edu. + +Now that you've got your key, you'll want to enable it by adding a new tenant to use the seed. #### Set Signing Key -Signing keys are set as 'seeds' in the [.signer.env](.signer.env) file for the signer-service. +Signing keys are set as 'seeds' in the [.signing-service.env](.signing-service.env) file for the `signing-service`. The default signing key is set as: @@ -368,7 +417,7 @@ We're calling these differents signing authorities 'tenants'. #### Add a Tenant Seed -Adding a tenant is simple. Just add another `TENANT_SEED_{TENANT_NAME}` environment variable in [.signer.env](.signer.env). The value of the variable should be a seed. Generate a new seed as explained in [Generate a new key](#generate-a-new-key), and set it as explained for the default tenant in [Set Signing Key](#set-signing-key). +Adding a tenant is simple. Just add another `TENANT_SEED_{TENANT_NAME}` environment variable in [.signing-service.env](.signing-service.env). The value of the variable should be a seed. Generate a new seed as explained in [Generate New Key](#generate-new-key), and set it as explained for the default tenant in [Set Signing Key](#set-signing-key). #### Declare Tenant Endpoints @@ -380,7 +429,7 @@ Add a `TENANT_TOKEN_{TENANT_NAME}` environment variable to the [.coordinator.env #### Tenants Example -To set up two tenants, one for degrees and one for completion of the Econ101 course, and you wanted to secure the degrees tenant but not the Econ101, then you could create the tenants by setting the following in the [.signer.env](.signer.env) file: +To set up two tenants, one for degrees and one for completion of the Econ101 course, and you wanted to secure the degrees tenant but not the Econ101, then you could create the tenants by setting the following in the [.signing-service.env](.signing-service.env) file: ``` TENANT_SEED_DEGREES=z1AoLPRWHSKasPH1unbY1A6ZFF2Pdzzp7D2CkpK6YYYdKTN @@ -403,7 +452,7 @@ http://myhost.org/instance/econ101/credentials/issue And since you set a token for the degrees tenant, you'll have to include that token in the auth header as a Bearer token. A curl command to issue on the degrees endpoint would then look like: ``` -curl --location 'http://localhost:4007/instance/degrees/credentials/issue' \ +curl --location 'http://localhost:4005/instance/degrees/credentials/issue' \ --header 'Authorization: Bearer 988DKLAJH93KDSFV' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -411,32 +460,33 @@ curl --location 'http://localhost:4007/instance/degrees/credentials/issue' \ }' ``` -### Enable Revocation - -The issuer provides an optional revocation (or 'status') mechanism that implements the [StatusList2021 specification](https://www.w3.org/TR/vc-status-list/), using Github to store the access list. So to use the list you'll have to create two new github repositories that will be used exclusively to manage the status. Full details of the implementation are [here](https://github.com/digitalcredentials/status-list-manager-git) +### Revocation and Suspension -For this MVP implementation of the issuer we've only exposed the github options, but if you would like to use gitlab instead, just let us know and we can expose those options. +The issuer provides an optional revocation/suspension mechanism that implements [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/), using [database services](https://github.com/digitalcredentials/credential-status-manager-db) or [Git services](https://github.com/digitalcredentials/status-list-manager-git) to store the status list. We recommend using the database implementation for production and test deployments and the Git implementation only for light testing/experimental purposes. -Revoking a credential is described in [Usage - revoking](#revoking) +To enable status updates, set `ENABLE_STATUS_SERVICE` to `true` in `.coordinator.env`. To perform revocations and suspensions, see the [Usage - Revoking and Suspending](#revoking-and-suspending) section below. -#### Create Github repositories +### Environment Variables -Create two repositories, one public and one private. Call them anything you like, but something like myproject-status-list (public) and myproject-status-list-meta (private) are good choices. If you need help, instructions are [here](https://github.com/digitalcredentials/credential-status-manager-git#create-credential-status-repositories) +These are all of the general environment variables that you will need to configure in `.coordinator.env`: -Get a Github token with access to the repositories as described [here](https://github.com/digitalcredentials/credential-status-manager-git#generate-access-tokens) +| Key | Description | Type | Required | +| --- | --- | --- | --- | +| `SIGNING_SERVICE` | domain of signing service | string | no (default: `SIGNER:4006`) | +| `STATUS_SERVICE` | domain of status service | string | no (default: `STATUS:4008`) | +| `TENANT_TOKEN_{TENANT_NAME}` | HTTP authorization bearer token to secure service endpoint access for a given tenant | string | yes | +| `PORT` | HTTP port on which to run the express app | number | no (default: `4005`) | +| `ENABLE_ACCESS_LOGGING` | whether to enable access logging | boolean | no (default: `true`) | +| `ENABLE_STATUS_SERVICE` | whether to enable status | boolean | no (default: `true`) | +| `ENABLE_HTTPS_FOR_DEV` | whether to enable HTTPS in a development instance of the app | boolean | no (default: `true`) | -Now set these in the [.status.env](.status.env) file, which has the following options: +These are the environment variables that you will need to configure in `.signing-service.env`: -| Key | Description | Default | Required | +| Key | Description | Type | Required | | --- | --- | --- | --- | -| `PORT` | http port on which to run the express app | 4008 | no | -| `CRED_STATUS_OWNER` | name of the owner account (personal or organization) in the source control service that will host the credential status resources | no | yes if ENABLE_STATUS_ALLOCATION is true | -| `CRED_STATUS_REPO_NAME` | name of the credential status repository | no | yes if ENABLE_STATUS_ALLOCATION is true | -| `CRED_STATUS_META_REPO_NAME` | name of the credential status metadata repository | no | yes if ENABLE_STATUS_ALLOCATION is true | -| `CRED_STATUS_ACCESS_TOKEN` | Github access token for the credential status repositories | no | yes if ENABLE_STATUS_ALLOCATION is true | -| `CRED_STATUS_DID_SEED` | seed used to deterministically generate DID | no | yes if ENABLE_STATUS_ALLOCATION is true | +| `TENANT_SEED_{TENANT_NAME}` | secret key deterministically associated with the issuer DID | string | yes | -The `CRED_STATUS_DID_SEED` is set to a default seed, usable by anyone for testing. You'll have to change that to use your own seed. Follow the instructions in [Generate a new Key](#generate-a-new-key) to generate a new key seed, and set the value (from the 'seed' property of the object returned from the seed generator). +In addition to the variables defined above, you will also need to provide environment bindings for status related configurations in `.status-service-db.env` or `.status-service-git.env`. Because there are two different implementations of a credential status manager - one for database storage and one for Git storage - you need to populate the appropriate file, depending on which one you want to use. For the database solution, please define at least the required fields specified [here](https://github.com/digitalcredentials/status-service-db/blob/main/README.md#environment-variables) and for the Git solution, please define at least the required fields specified [here](https://github.com/digitalcredentials/status-service-git/blob/main/README.md#environment-variables). ### DID Registries @@ -466,71 +516,52 @@ There are many ways to incorporate this system into your own flows. A typical fl TODO: DESCRIBE other POSSIBLE CONFIGURATIONS -### Revoking +### Revoking and Suspending -Revocation is more fully explained in the StatusList2021 specification and the DCC [git based status implemenation](https://github.com/digitalcredentials/credential-status-manager-git), but it amounts to POSTing an object to the revocation endpoint, like so: +Revocation and suspension are more fully explained in the [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/) specification and our implemenations thereof, but effectively, it amounts to POSTing an object to the status update endpoint, like so: -``` -{ - credentialId: 'id_added_by_status_manager_to_credentialStatus_propery_of_VC', - credentialStatus: [{ - type: 'StatusList2021Credential', - status: 'revoked' +```bash +curl --location 'http://localhost:4005/instance/test/credentials/status' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "credentialStatus": [{ + "type": "BitstringStatusListCredential", + "status": "revoked" }] -} -``` - -The important part there is the `credentialId`, which is listed in the `credentialStatus` section of the issued credential (`credentialStatus` is added by the status service), and which you have to store at the point when you issue the credential. The `credentialStatus` section looks like this: - -``` -"credentialStatus": { - "id": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16", - "type": "StatusList2021Entry", - "statusPurpose": "revocation", - "statusListIndex": 16, - "statusListCredential": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4" - } +}' ``` -and the id you need is in the `id` property. +The important part there is the `credentialId`. If an issuer provides an `id` field on a credential, the status service will pick this up and save the credential under this ID, as long as it is a valid VC ID, per [these guidelines](https://www.w3.org/TR/vc-data-model-2.0/#identifiers) (e.g., URL, URN). If an ID is not provided, the status service will automatically generate one and attach it to the credential as the `id` field. -So again, the important point here is that you must store the credentialStatus.id for all credentials that you issue. A common approach might be to add another column to whatever local database you are using for your credential records, which would then later make it easier for you to find the id you need by searching the other fields like student name or student id. +It is important that you save this value in your system during the issuance process, as you will need it to perform revocations and suspensions in the future. A common approach might be to add another column to whatever local database you are using for your credential records, which would then later make it easier for you to find the ID you need by searching the other fields like student name or student ID. -NOTE: you'll of course have to have [set up revocation](#enable-revocation) for this to work. If you've only done the QuickStart then you'll not be able to revoke. +**Note:** You'll of course have to enable [status updates](#revocation-and-suspension) for this to work. If you've only done the Quick Start then you'll not be able to revoke and suspend. ## Development -To run the exchange-coordinator locally from the cloned repository (rather than using docker compose to pull in docker hub images), you'll also have to clone the repository for the other services that this coordinator calls: - -* [signing-service](https://github.com/digitalcredentials/signing-service) -* [transaction-service](https://github.com/digitalcredentials/transaction-service) - -and optionally, if you are allocating a status position for revocation: - -* [status-service](https://github.com/digitalcredentials/signing-service) +To run the `workflow-coordinator` locally from the cloned repository, you'll also have to clone the repository for the [signing-service](https://github.com/digitalcredentials/signing-service) and the [transaction-service](https://github.com/digitalcredentials/transaction-service) and have them running locally at the same time. Additionally, if you want to include status allocation, you'll also have to clone one of the status service repositories: [status-service-db](https://github.com/digitalcredentials/status-service-db), [status-service-git](https://github.com/digitalcredentials/status-service-git). -and have them all running locally at the same time. - -When running locally, the system picks up environment variables from the standard [.env](./.env) file, rather than from the env files that we recommend using with docker compose. +When running locally, the system picks up environment variables from the standard [.env](.env) file, rather than from the `.env` files that we recommend using with Docker Compose. ### Installation -Clone code then cd into directory and: +Clone code, cd into the directory, and run: -``` +```bash npm install npm run dev ``` -If for whatever reason you need to run the server over https, you can set the `ENABLE_HTTPS_FOR_DEV` environment variable to true. Note, though, that this should ONLY be used for development. - ### Testing -Testing uses supertest, mocha, and nock to test the endpoints. To run tests: +Testing uses `supertest`, `mocha`, and `nock` to test the endpoints. To run tests: -```npm run test``` +```bash +npm run test +``` -This coordinator coordinates http calls out to other services, but rather than have to make these calls for every test, and possibly in cases where outgoing http calls aren't ideal, we've used [nock](https://github.com/nock/nock) to mock out the http calls so that the actual calls needn't be made - nock instead returns our precanned replies. +Note that when testing we don't actually want to make live HTTP calls to the services, so we've used Nock to intercept the HTTP calls and return precanned data. ## Contribute diff --git a/docker-compose.yml b/docker-compose.yml index 8b6c97d..181f029 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,23 @@ version: '3.5' services: coordinator: - image: digitalcredentials/exchange-coordinator:0.1.0 - # env_file: - # - ./.coordinator.env + image: digitalcredentials/workflow-coordinator:0.1.0 + env_file: + - ./.coordinator.env ports: - "4005:4005" signing: - image: digitalcredentials/signing-service:0.1.0 - # env_file: - # - ./.signing-service.env - transactions: + image: digitalcredentials/signing-service:0.1.0 + env_file: + - ./.signing-service.env + transaction: image: digitalcredentials/transaction-service:0.1.0 - # env_file: - # - ./.transactions-service.env - #status: - # image: digitalcredentials/status-service:0.1.0 - # env_file: - # - ./.status-service.env \ No newline at end of file + env_file: + - ./.transaction-service.env + status: + image: digitalcredentials/status-service-db:0.1.0 + # image: digitalcredentials/status-service-git:0.1.0 + env_file: + - ./.status-service-db.env + # - ./.status-service-git.env + \ No newline at end of file diff --git a/package.json b/package.json index 3ae14b9..9b1ea8e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@digitalcredentials/exchange-coordinator", + "name": "@digitalcredentials/workflow-coordinator", "version": "0.0.0", "private": true, "type": "module", @@ -47,8 +47,8 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/digitalcredentials/exchange-coordinator" + "url": "https://github.com/digitalcredentials/workflow-coordinator" }, - "homepage": "https://github.com/digitalcredentials/exchange-coordinator", - "bugs": "https://github.com/digitalcredentials/exchange-coordinator/issues" + "homepage": "https://github.com/digitalcredentials/workflow-coordinator", + "bugs": "https://github.com/digitalcredentials/workflow-coordinator/issues" } diff --git a/src/app.js b/src/app.js index b949d12..40389c2 100644 --- a/src/app.js +++ b/src/app.js @@ -1,7 +1,7 @@ import express from 'express'; import cors from 'cors'; import axios from 'axios'; -import https from "https" +import https from 'https' import accessLogger from './middleware/accessLogger.js'; import errorHandler from './middleware/errorHandler.js'; import errorLogger from './middleware/errorLogger.js'; @@ -25,7 +25,7 @@ async function callService(endpoint, body) { export async function build(opts = {}) { - const { enableStatusService, coordinatorServiceEndpoint, statusServiceEndpoint, signingServiceEndpoint, transactionServiceEndpoint, exchangeHost } = getConfig(); + const { enableStatusService, coordinatorService, statusService, signingService, transactionService, exchangeHost } = getConfig(); var app = express(); // Add the middleware to write access logs app.use(accessLogger()); @@ -36,7 +36,7 @@ export async function build(opts = {}) { app.get('/', async function (req, res, next) { if (enableStatusService) { try { - await axios.get(`http://${statusServiceEndpoint}/`) + await axios.get(`http://${statusService}/`) } catch (e) { next({ message: 'status service is NOT running.', @@ -46,7 +46,7 @@ export async function build(opts = {}) { } } try { - await axios.get(`http://${signingServiceEndpoint}/`) + await axios.get(`http://${signingService}/`) } catch (e) { next({ message: 'signing service is NOT running.', @@ -55,7 +55,7 @@ export async function build(opts = {}) { }) } try { - await axios.get(`http://${transactionServiceEndpoint}/`) + await axios.get(`http://${transactionService}/`) } catch (e) { next({ message: 'transaction service is NOT running.', @@ -64,17 +64,15 @@ export async function build(opts = {}) { }) } const message = enableStatusService ? - "exchange-coordinator, status-service, transaction-service, and signing-service all ok." - : - "exchange-coordinator, transaction-service, and signing-service all ok. status-service is disabled." + 'workflow-coordinator, status-service, transaction-service, and signing-service all ok.' : + 'workflow-coordinator, transaction-service, and signing-service all ok. status-service is disabled.' res.status(200).send({ message }) - }); /* A test that mocks the 'issuer coordinator app', i.e, the client code - that a university would write to invoke the exchange-coordinator. this + that a university would write to invoke the workflow-coordinator. this should be called from a web browser on a phone (or emulator) because it will redirect to the Learner Credential Wallet running on the phone. @@ -82,7 +80,7 @@ export async function build(opts = {}) { app.get('/demo', async (req, res, next) => { const retrievalId = 'ohmy' // 1. post the test vc to the /setup endpoint - const data = {tenantName: "test", data: [{ vc: testVC, retrievalId: 'ohmy' }]} + const data = {tenantName: 'test', data: [{ vc: testVC, retrievalId: 'ohmy' }]} const walletQuerys = await callService(`${exchangeHost}/exchange/setup`, data) // The exchange endpoint stores the data and returns a deeplink (with challenge) // to which the student can be redirected, and which will then open the wallet @@ -109,14 +107,13 @@ export async function build(opts = {}) { * which to then initiate the exchange. * Note that by setting 'flow=direct' on the object posted * to this endpoint, we can tell the exchanger to skip the - * inititation step and give the wallet the endpoint to which + * initiation step and give the wallet the endpoint to which * to send the DIDAuth. This is for backward compatability. */ - app.post("/exchange/setup", + app.post('/exchange/setup', async (req, res, next) => { try { //const tenantName = req.params.tenantName //the issuer instance/tenant with which to sign -console.log("in the exchange setup") const exchangeData = req.body; // TODO: CHECK THE INCOMING DATA FOR CORRECTNESS HERE if (!exchangeData || !Object.keys(exchangeData).length) throw new CoordinatorException(400, 'You must provide data for the exchange. Check the README for details.') @@ -124,10 +121,9 @@ console.log("in the exchange setup") await verifyAuthHeader(req.headers.authorization, exchangeData.tenantName) - const walletQuerys = await callService(`http://${transactionServiceEndpoint}/exchange`, exchangeData) + const walletQuerys = await callService(`http://${transactionService}/exchange`, exchangeData) return res.json(walletQuerys) - } catch (error) { // catch async errors and forward error handling // middleware @@ -144,11 +140,11 @@ console.log("in the exchange setup") * Note that this step can be skipped by setting * flow=direct in the object we pass in to the setup endpoint. */ - app.post("/exchange/:exchangeId", + app.post('/exchange/:exchangeId', async (req, res, next) => { try { const exchangeId = req.params.exchangeId - const vpr = await callService(`http://${transactionServiceEndpoint}/exchange/${exchangeId}/`) + const vpr = await callService(`http://${transactionService}/exchange/${exchangeId}/`) return res.json(vpr) } catch (error) { @@ -167,7 +163,7 @@ console.log("in the exchange setup") * the status service and signing service before returning * to the wallet. */ - app.post("/exchange/:exchangeId/:transactionId", + app.post('/exchange/:exchangeId/:transactionId', async (req, res, next) => { try { const exchangeId = req.params.exchangeId @@ -177,7 +173,7 @@ console.log("in the exchange setup") // make a deep copy of the didAuth because something seemed to be // going wrong with the streams const didAuth = JSON.parse(JSON.stringify(body)) - const transactionEndpoint = `http://${transactionServiceEndpoint}/exchange/${exchangeId}/${transactionId}` + const transactionEndpoint = `http://${transactionService}/exchange/${exchangeId}/${transactionId}` const response = await axios.post(transactionEndpoint, didAuth); const { data } = response const { tenantName, vc: unSignedVC } = data @@ -185,12 +181,12 @@ console.log("in the exchange setup") // add credential status if enabled const vcReadyToSign = enableStatusService ? - await callService(`http://${statusServiceEndpoint}/credentials/status/allocate`, unSignedVC) + await callService(`http://${statusService}/credentials/status/allocate`, unSignedVC) : unSignedVC // sign the credential - const signedVC = await callService(`http://${signingServiceEndpoint}/instance/${tenantName.toLowerCase()}/credentials/sign`, vcReadyToSign) + const signedVC = await callService(`http://${signingService}/instance/${tenantName.toLowerCase()}/credentials/sign`, vcReadyToSign) return res.json(signedVC) } catch (error) { @@ -201,13 +197,13 @@ console.log("in the exchange setup") }) // updates the status - // the body will look like: {credentialId: '23kdr', credentialStatus: [{type: 'StatusList2021Credential', status: 'revoked'}]} - app.post("/instance/:tenantName/credentials/status", + // the body will look like: {credentialId: '23kdr', credentialStatus: [{type: 'BitstringStatusListCredential', status: 'revoked'}]} + app.post('/instance/:tenantName/credentials/status', async (req, res, next) => { - if (!enableStatusService) return res.status(405).send("The status service has not been enabled.") + if (!enableStatusService) return res.status(405).send('The status service has not been enabled.') try { await verifyAccess(req.headers.authorization, req.params.tenantName) - const updateResult = await callService(`http://${statusServiceEndpoint}/instance/${tenantName}/credentials/sign`, vcWithStatus) + const updateResult = await callService(`http://${statusService}/instance/${tenantName}/credentials/sign`, vcWithStatus) return res.json(updateResult) } catch (error) { // have to catch and forward async errors to middleware: @@ -216,7 +212,7 @@ console.log("in the exchange setup") }) app.get('/seedgen', async (req, res, next) => { - const response = await axios.get(`http://${signingServiceEndpoint}/seedgen`) + const response = await axios.get(`http://${signingService}/seedgen`) return res.json(response.data) }); diff --git a/src/app.test.js b/src/app.test.js index 5de4d3d..0cb8fb0 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -40,7 +40,7 @@ describe('api', () => { unprotectedTenantToken = process.env[`TENANT_TOKEN_${unprotectedTenantName}`] protectedTenantToken = process.env[`TENANT_TOKEN_${protectedTenantName}`] randomToken = process.env[`TENANT_TOKEN_${randomTenantName}`] - statusUpdateBody = { "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", "credentialStatus": [{ "type": "StatusList2021Credential", "status": "revoked" }] } + statusUpdateBody = { "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", "credentialStatus": [{ "type": "BitstringStatusListCredential", "status": "revoked" }] } }); after(() => { @@ -65,13 +65,13 @@ describe('api', () => { nock('http://localhost:4006').get("/").reply(200, 'signing-service server status: ok.') nock('http://localhost:4008').get("/").reply(200, 'status-service server status: ok.') - nock('http://localhost:4004').get("/").reply(200, 'transaction-manager-service server status: ok.') + nock('http://localhost:4004').get("/").reply(200, 'transaction-service server status: ok.') request(app) .get("/") .expect(200) .expect('Content-Type', /json/) - .expect(/{"message":"exchange-coordinator, transaction-service, and signing-service all ok. status-service is disabled."}/, done); + .expect(/{"message":"workflow-coordinator, transaction-service, and signing-service all ok. status-service is disabled."}/, done); }); }) @@ -198,7 +198,7 @@ describe('api', () => { /* A test that mocks both the wallet part of the interaction, and the 'issuer coordinator app' (i.e, the university specific code that uses the - exchange coordinator), so that the flow can more easily be + workflow coordinator), so that the flow can more easily be tested, without needing to redirect into an actual running wallet. NOTE: THIS TESTS WITHOUT THE INTERMEIDATE VPR step */ @@ -210,7 +210,7 @@ describe('api', () => { const exchangeSetupData = getDataForExchangeSetupPost(unprotectedTenantName) const response = await request(app) - .post("/exchange/setup") + .post(exchangeSetupPath) .send(exchangeSetupData) expect(response.header["content-type"]).to.have.string("json"); expect(response.status).to.eql(200); @@ -221,7 +221,7 @@ describe('api', () => { const url = walletQuery.directDeepLink // Step 2. Now that we've got the deeplink, we would redirect to it, which - // would open the wallet. In this test, we're just assuming that this has already + // would open the wallet. In this test, we're just assuming that this has already // happened, and that now we are in the wallet // Step 3. Mimics what the wallet would do when opened by deeplink @@ -259,7 +259,7 @@ describe('api', () => { /* A test that mocks both the wallet part of the interaction, and the 'issuer coordinator app' (i.e, the university specific code that uses the - exchange coordinator), so that the flow can be + workflow coordinator), so that the flow can be tested without needing to redirect into an actual running wallet. NOTE: this tests with the intermediate VPR step */ @@ -269,7 +269,7 @@ describe('api', () => { // to which the student can be redirected, and which will then open the wallet const setupResponse = await request(app) - .post("/exchange/setup") + .post(exchangeSetupPath) .send(getDataForExchangeSetupPost(unprotectedTenantName)) expect(setupResponse.header["content-type"]).to.have.string("json"); expect(setupResponse.status).to.eql(200); @@ -285,10 +285,10 @@ describe('api', () => { // Step 2. mimics what the wallet would do when opened by deeplink // which is to parse the deeplink and call the exchange initiation endpoint const parsedDeepLink = new URL(url) - const inititationURI = parsedDeepLink.searchParams.get('vc_request_url'); + const initiationURI = parsedDeepLink.searchParams.get('vc_request_url'); // strip out the host because we are using supertest - const initiationURIPath = (new URL(inititationURI)).pathname + const initiationURIPath = (new URL(initiationURI)).pathname const initiationResponse = await request(app) .post(initiationURIPath) @@ -323,8 +323,5 @@ describe('api', () => { //expect(signedVC.credentialSubject.id).to.equal(randomId) }) - }) - - -}) \ No newline at end of file +}) diff --git a/src/config.js b/src/config.js index b04d7df..39e433b 100644 --- a/src/config.js +++ b/src/config.js @@ -6,17 +6,17 @@ const randomTenantToken = "UNPROTECTED" const defaultTenantToken = "UNPROTECTED" const defaultExchangeHost = "http://coordinator:4005" -const defaultCoordinatorServiceEndpoint = "COORDINATOR:4005" -const defaultStatusServiceEndpoint = "STATUS:4008" -const defaultSigningServiceEndpoint = "SIGNING:4006" -const defaultTransactionServiceEndpoint = "TRANSACTIONS:4004" +const defaultCoordinatorService = "COORDINATOR:4005" +const defaultStatusService = "STATUS:4008" +const defaultSigningService = "SIGNING:4006" +const defaultTransactionService = "TRANSACTIONS:4004" // when developing using locally run, i.e, without docker-compose //const defaultExchangeHost = "http://localhost:4005" -//const defaultCoordinatorServiceEndpoint = "localhost:4005" -//const defaultStatusServiceEndpoint = "localhost:4008" -//const defaultSigningServiceEndpoint = "localhost:4006" -//const defaultTransactionServiceEndpoint = "localhost:4004" +//const defaultCoordinatorService = "localhost:4005" +//const defaultStatusService = "localhost:4008" +//const defaultSigningService = "localhost:4006" +//const defaultTransactionService = "localhost:4004" // we set a default tenant // It will be overwritten by whatever value is set for default in .env @@ -49,10 +49,10 @@ function parseConfig() { enableHttpsForDev: env.ENABLE_HTTPS_FOR_DEV?.toLowerCase() === 'true', enableAccessLogging: env.ENABLE_ACCESS_LOGGING?.toLowerCase() === 'true', enableStatusService: env.ENABLE_STATUS_SERVICE?.toLowerCase() === 'true', - coordinatorServiceEndpoint: env.COORDINATOR_SERVICE_ENDPOINT ? env.COORDINATOR_SERVICE_ENDPOINT : defaultCoordinatorServiceEndpoint, - statusServiceEndpoint: env.STATUS_SERVICE_ENDPOINT ? env.STATUS_SERVICE_ENDPOINT : defaultStatusServiceEndpoint, - signingServiceEndpoint: env.SIGNING_SERVICE_ENDPOINT ? env.SIGNING_SERVICE_ENDPOINT : defaultSigningServiceEndpoint, - transactionServiceEndpoint: env.TRANSACTION_SERVICE_ENDPOINT ? env.TRANSACTION_SERVICE_ENDPOINT : defaultTransactionServiceEndpoint, + coordinatorService: env.COORDINATOR_SERVICE ? env.COORDINATOR_SERVICE : defaultCoordinatorService, + statusService: env.STATUS_SERVICE ? env.STATUS_SERVICE : defaultStatusService, + signingService: env.SIGNING_SERVICE ? env.SIGNING_SERVICE : defaultSigningService, + transactionService: env.TRANSACTION_SERVICE ? env.TRANSACTION_SERVICE : defaultTransactionService, exchangeHost: env.PUBLIC_EXCHANGE_HOST ? env.PUBLIC_EXCHANGE_HOST : defaultExchangeHost, port: env.PORT ? parseInt(env.PORT) : defaultPort }); diff --git a/src/test-fixtures/.env.testing b/src/test-fixtures/.env.testing index 64ecf04..f3ebf68 100644 --- a/src/test-fixtures/.env.testing +++ b/src/test-fixtures/.env.testing @@ -11,9 +11,9 @@ # ENABLE_STATUS_SERVICE=true # set the service endpoints -# defaults are as follows -# STATUS_SERVICE_ENDPOINT=STATUS:4008 -# SIGNING_SERVICE_ENDPOINT=SIGNER:4006 +# defaults are as follows +# STATUS_SERVICE=STATUS:4008 +# SIGNING_SERVICE=SIGNER:4006 # Tokens for protecting tenant endpoints. # Add a token for any tenant name, @@ -35,15 +35,15 @@ TENANT_TOKEN_RANDOM_TEST=UNPROTECTED # set the service endpoints # default to the default docker compose service names -# STATUS_SERVICE_ENDPOINT=STATUS:4008 -# SIGNING_SERVICE_ENDPOINT=SIGNER:4006 -# TRANSACTION_SERVICE_ENDPOINT=SIGNER:4004 -# COORDINATOR_SERVICE_ENDPOINT=SIGNER:4005 +# STATUS_SERVICE=STATUS:4008 +# SIGNING_SERVICE=SIGNER:4006 +# TRANSACTION_SERVICE=SIGNER:4004 +# COORDINATOR_SERVICE=SIGNER:4005 # use localhost for service endpoints # when developing locally -STATUS_SERVICE_ENDPOINT=localhost:4008 -SIGNING_SERVICE_ENDPOINT=localhost:4006 -TRANSACTION_SERVICE_ENDPOINT=localhost:4004 -COORDINATOR_SERVICE_ENDPOINT=localhost:4005 +STATUS_SERVICE=localhost:4008 +SIGNING_SERVICE=localhost:4006 +TRANSACTION_SERVICE=localhost:4004 +COORDINATOR_SERVICE=localhost:4005 PUBLIC_EXCHANGE_HOST=http://localhost:4005 diff --git a/src/test-fixtures/nocks/directTest.js b/src/test-fixtures/nocks/directTest.js index 1e426fa..5b366e3 100644 --- a/src/test-fixtures/nocks/directTest.js +++ b/src/test-fixtures/nocks/directTest.js @@ -1,9 +1,147 @@ import nock from 'nock'; +const unsignedVc = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json", + "https://w3id.org/vc/status-list/2021/v1" + ], + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC", + "type": "Profile", + "name": "University of Wonderful", + "description": "The most wonderful university", + "url": "https://wonderful.edu/", + "image": { + "id": "https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png", + "type": "Image" + } + }, + "issuanceDate": "2020-01-01T00:00:00Z", + "name": "A Simply Wonderful Course", + "credentialSubject": { + "type": "AchievementSubject", + "achievement": { + "id": "http://wonderful.wonderful", + "type": "Achievement", + "criteria": { + "narrative": "Completion of the Wonderful Course - well done you!" + }, + "description": "Wonderful.", + "name": "Introduction to Wonderfullness" + } + } +}; + +const unsignedVcWithId = { + ...unsignedVc, + credentialSubject: { + ...unsignedVc.credentialSubject, + id: 'did:ex:223234' + } +}; + +const signedVcWithId = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json", + "https://w3id.org/vc/status-list/2021/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "achievement": { + "criteria": { + "narrative": "Completion of the Wonderful Course - well done you!" + }, + "description": "Wonderful.", + "id": "http://wonderful.wonderful", + "name": "Introduction to Wonderfullness", + "type": "Achievement" + }, + "id": "did:ex:223234", + "type": "AchievementSubject" + }, + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "issuanceDate": "2020-01-01T00:00:00Z", + "issuer": { + "description": "The most wonderful university", + "id": "did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy", + "image": { + "id": "https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png", + "type": "Image" + }, + "name": "University of Wonderful", + "type": "Profile", + "url": "https://wonderful.edu/" + }, + "name": "A Simply Wonderful Course", + "proof": { + "created": "2023-09-11T18:39:02Z", + "proofPurpose": "assertionMethod", + "proofValue": "z4J4QCmsJBBeC3kTY9rBDQMJCfz7p8ZZ5ELEbiDcY5z2EHzedF1kdDKthdhrzvindUhg3A4Z9Bs9tTX1hQ7gzTwo7", + "type": "Ed25519Signature2020", + "verificationMethod": "did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy#z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy" + }, + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ] +}; + +const signedVp = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "holder": "did:ex:223234", + "proof": { + "type": "Ed25519Signature2020", + "created": /.+/i, + "verificationMethod": "did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP#z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP", + "proofPurpose": "authentication", + "challenge": "9374011d-2b48-4416-a7a8-ea7a50b155a8", + "proofValue": /.+/i + } +}; + export default () => { - nock('http://localhost:4004', {"encodedQueryParams":true}) - .post('/exchange', {"tenantName":"UN_PROTECTED_TEST","data":[{"vc":{"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"}}},"retrievalId":"someId"}],"exchangeHost":"http://localhost:4005"}) - .reply(200, [{"retrievalId":"someId","directDeepLink":"https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=9374011d-2b48-4416-a7a8-ea7a50b155a8&vc_request_url=http://localhost:4005/exchange/8c6f8343-e82b-48a2-b81e-3c9e0d596238/9374011d-2b48-4416-a7a8-ea7a50b155a8","vprDeepLink":"https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&vc_request_url=http://localhost:4005/exchange/8c6f8343-e82b-48a2-b81e-3c9e0d596238","chapiVPR":{"query":{"type":"DIDAuthentication"},"interact":{"service":[{"type":"VerifiableCredentialApiExchangeService","serviceEndpoint":"http://localhost:4005/exchange/8c6f8343-e82b-48a2-b81e-3c9e0d596238/9374011d-2b48-4416-a7a8-ea7a50b155a8"},{"type":"CredentialHandlerService"}]},"challenge":"9374011d-2b48-4416-a7a8-ea7a50b155a8","domain":"http://localhost:4005"}}], [ + nock('http://localhost:4004', { encodedQueryParams: true }) + .post('/exchange', { + "tenantName": "UN_PROTECTED_TEST", + "data": [{ "vc": unsignedVc, "retrievalId": "someId" }], + "exchangeHost": "http://localhost:4005" + }) + .reply(200, [{ + "retrievalId": "someId", + "directDeepLink": "https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=9374011d-2b48-4416-a7a8-ea7a50b155a8&vc_request_url=http://localhost:4005/exchange/8c6f8343-e82b-48a2-b81e-3c9e0d596238/9374011d-2b48-4416-a7a8-ea7a50b155a8", + "vprDeepLink":"https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&vc_request_url=http://localhost:4005/exchange/8c6f8343-e82b-48a2-b81e-3c9e0d596238", + "chapiVPR": { + "query": { + "type": "DIDAuthentication" + }, + "interact": { + "service":[ + { + "type": "VerifiableCredentialApiExchangeService", + "serviceEndpoint": "http://localhost:4005/exchange/8c6f8343-e82b-48a2-b81e-3c9e0d596238/9374011d-2b48-4416-a7a8-ea7a50b155a8" + }, + { + "type": "CredentialHandlerService" + } + ] + }, + "challenge": "9374011d-2b48-4416-a7a8-ea7a50b155a8", + "domain": "http://localhost:4005" + } + }], [ 'X-Powered-By', 'Express', 'Access-Control-Allow-Origin', @@ -25,9 +163,16 @@ export default () => { // NOTE: the DIDAuth that we specify in the body of this post uses a regex wildcard (/.+/i) // for the 'created' date and the 'proofValue' since those vary for each new DIDAuth // that we generate as part of the test -nock('http://localhost:4004', {"encodedQueryParams":true}) - .post('/exchange/8c6f8343-e82b-48a2-b81e-3c9e0d596238/9374011d-2b48-4416-a7a8-ea7a50b155a8', {"@context":["https://www.w3.org/2018/credentials/v1","https://w3id.org/security/suites/ed25519-2020/v1"],"type":["VerifiablePresentation"],"holder":"did:ex:223234","proof":{"type":"Ed25519Signature2020","created":/.+/i,"verificationMethod":"did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP#z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP","proofPurpose":"authentication","challenge":"9374011d-2b48-4416-a7a8-ea7a50b155a8","proofValue":/.+/i}}) - .reply(200, {"vc":{"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"}}},"retrievalId":"someId","tenantName":"UN_PROTECTED_TEST","exchangeHost":"http://localhost:4005","transactionId":"9374011d-2b48-4416-a7a8-ea7a50b155a8","exchangeId":"8c6f8343-e82b-48a2-b81e-3c9e0d596238"}, [ +nock('http://localhost:4004', { encodedQueryParams: true }) + .post('/exchange/8c6f8343-e82b-48a2-b81e-3c9e0d596238/9374011d-2b48-4416-a7a8-ea7a50b155a8', signedVp) + .reply(200, { + "vc": unsignedVc, + "retrievalId": "someId", + "tenantName": "UN_PROTECTED_TEST", + "exchangeHost": "http://localhost:4005", + "transactionId": "9374011d-2b48-4416-a7a8-ea7a50b155a8", + "exchangeId": "8c6f8343-e82b-48a2-b81e-3c9e0d596238" + }, [ 'X-Powered-By', 'Express', 'Access-Control-Allow-Origin', @@ -46,9 +191,9 @@ nock('http://localhost:4004', {"encodedQueryParams":true}) 'timeout=5' ]); -nock('http://localhost:4006', {"encodedQueryParams":true}) - .post('/instance/un_protected_test/credentials/sign', {"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"},"id":"did:ex:223234"}}) - .reply(200, {"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1","https://w3id.org/security/suites/ed25519-2020/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"},"id":"did:ex:223234"},"proof":{"type":"Ed25519Signature2020","created":"2023-09-11T18:39:02Z","verificationMethod":"did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy#z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy","proofPurpose":"assertionMethod","proofValue":"z4J4QCmsJBBeC3kTY9rBDQMJCfz7p8ZZ5ELEbiDcY5z2EHzedF1kdDKthdhrzvindUhg3A4Z9Bs9tTX1hQ7gzTwo7"}}, [ +nock('http://localhost:4006', { encodedQueryParams: true }) + .post('/instance/un_protected_test/credentials/sign', unsignedVcWithId) + .reply(200, signedVcWithId, [ 'X-Powered-By', 'Express', 'Access-Control-Allow-Origin', @@ -67,9 +212,23 @@ nock('http://localhost:4006', {"encodedQueryParams":true}) 'timeout=5' ]); } - /* nock('http://localhost:4004', {"encodedQueryParams":true}) - .post('/exchange', {"vc":{"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"}}},"tenantName":"UN_PROTECTED_TEST","exchangeHost":"http://localhost:4005"}) - .reply(200, [{"type":"directDeepLink","url":"https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=3bc9dc5e-c949-4d1d-872e-e96797f49959&vc_request_url=http://localhost:4005/exchange/e8caa01f-3804-4fbf-a25c-3fbf280ca902/3bc9dc5e-c949-4d1d-872e-e96797f49959"},{"type":"vprDeepLink","url":"https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&vc_request_url=http://localhost:4005/exchange/e8caa01f-3804-4fbf-a25c-3fbf280ca902"}], [ +/* +nock('http://localhost:4004', { encodedQueryParams: true }) + .post('/exchange', { + "vc": unsignedVc, + "tenantName": "UN_PROTECTED_TEST", + "exchangeHost": "http://localhost:4005" + }) + .reply(200, [ + { + "type":"directDeepLink", + "url":"https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=3bc9dc5e-c949-4d1d-872e-e96797f49959&vc_request_url=http://localhost:4005/exchange/e8caa01f-3804-4fbf-a25c-3fbf280ca902/3bc9dc5e-c949-4d1d-872e-e96797f49959" + }, + { + "type": "vprDeepLink", + "url":"https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&vc_request_url=http://localhost:4005/exchange/e8caa01f-3804-4fbf-a25c-3fbf280ca902" + } + ], [ 'X-Powered-By', 'Express', 'Access-Control-Allow-Origin', @@ -91,9 +250,15 @@ nock('http://localhost:4006', {"encodedQueryParams":true}) // NOTE: the DIDAuth that we specify in the body of this post uses a regex wildcard (/.+/i) // for the 'created' date and the 'proofValue' since those vary for each new DIDAuth // that we generate as part of the test -nock('http://localhost:4004', {"encodedQueryParams":true}) - .post('/exchange/e8caa01f-3804-4fbf-a25c-3fbf280ca902/3bc9dc5e-c949-4d1d-872e-e96797f49959', {"@context":["https://www.w3.org/2018/credentials/v1","https://w3id.org/security/suites/ed25519-2020/v1"],"type":["VerifiablePresentation"],"holder":"did:ex:223234","proof":{"type":"Ed25519Signature2020","created":/.+/i,"verificationMethod":"did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP#z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP","proofPurpose":"authentication","challenge":"3bc9dc5e-c949-4d1d-872e-e96797f49959","proofValue":/.+/i}}) - .reply(200, {"vc":{"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"}}},"tenantName":"UN_PROTECTED_TEST","exchangeHost":"http://localhost:4005","transactionId":"3bc9dc5e-c949-4d1d-872e-e96797f49959","exchangeId":"e8caa01f-3804-4fbf-a25c-3fbf280ca902"}, [ +nock('http://localhost:4004', { encodedQueryParams: true }) + .post('/exchange/e8caa01f-3804-4fbf-a25c-3fbf280ca902/3bc9dc5e-c949-4d1d-872e-e96797f49959', signedVp) + .reply(200, { + "vc": unsignedVc, + "tenantName": "UN_PROTECTED_TEST", + "exchangeHost": "http://localhost:4005", + "transactionId": "3bc9dc5e-c949-4d1d-872e-e96797f49959", + "exchangeId": "e8caa01f-3804-4fbf-a25c-3fbf280ca902" + }, [ 'X-Powered-By', 'Express', 'Access-Control-Allow-Origin', @@ -112,9 +277,9 @@ nock('http://localhost:4004', {"encodedQueryParams":true}) 'timeout=5' ]); -nock('http://localhost:4006', {"encodedQueryParams":true}) - .post('/instance/un_protected_test/credentials/sign', {"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"},"id":"did:ex:223234"}}) - .reply(200, {"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1","https://w3id.org/security/suites/ed25519-2020/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"},"id":"did:ex:223234"},"proof":{"type":"Ed25519Signature2020","created":"2023-08-29T14:10:25Z","verificationMethod":"did:key:z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy#z6Mkf2rgv7ef8FmLJ5Py87LMa7nofQgv6AstdkgsXiiCUJEy","proofPurpose":"assertionMethod","proofValue":"z3xg7VSk4kuHh16KiUUMn1npokR9KEwG5EjPuSix47RKFU5oBkWLRv5usp4f1CwZJCsPAfHLJKKTfWeN5D4cF2Ljh"}}, [ +nock('http://localhost:4006', { encodedQueryParams: true }) + .post('/instance/un_protected_test/credentials/sign', unsignedVcWithId) + .reply(200, signedVcWithId, [ 'X-Powered-By', 'Express', 'Access-Control-Allow-Origin', @@ -132,13 +297,4 @@ nock('http://localhost:4006', {"encodedQueryParams":true}) 'Keep-Alive', 'timeout=5' ]); - - - - - - - - - - */ \ No newline at end of file +*/ diff --git a/src/test-fixtures/vc.js b/src/test-fixtures/vc.js index 2600dea..61d4050 100644 --- a/src/test-fixtures/vc.js +++ b/src/test-fixtures/vc.js @@ -1,15 +1,13 @@ import testVC from './testVC.js'; - // "credentialStatus": - const credentialStatus = { - "id": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16", - "type": "StatusList2021Entry", - "statusPurpose": "revocation", - "statusListIndex": 16, - "statusListCredential": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4" +const credentialStatus = { + "id": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4#16", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": 16, + "statusListCredential": "https://digitalcredentials.github.io/credential-status-jc-test/XA5AAK1PV4" } - const getUnsignedVC = () => JSON.parse(JSON.stringify(testVC)) const getUnsignedVCWithoutSuiteContext = () => { const vcCopy = JSON.parse(JSON.stringify(testVC))