diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 91% rename from .eslintrc.js rename to .eslintrc.cjs index ec4f13b..b371d40 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -14,6 +14,7 @@ module.exports = { '@typescript-eslint/prefer-ts-expect-error': 'off', '@typescript-eslint/return-await': 'off', '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', 'no-empty-pattern': 'off' } } diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 06c3297..342814e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,39 +8,43 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install - name: Run test with Node.js ${{ matrix.node-version }} - run: npm run test-node + run: npm run coveralls + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ github.token }} env: CI: true - test-karma: - runs-on: ubuntu-latest -# needs: [lint] - timeout-minutes: 10 - strategy: - matrix: - node-version: [16.x] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - name: Run karma tests - run: npm run test-karma +# test-karma: + # runs-on: ubuntu-latest +## needs: [lint] +# timeout-minutes: 10 + # strategy: + # matrix: + # node-version: [18.x] + # steps: + # - uses: actions/checkout@v2 +# - name: Use Node.js ${{ matrix.node-version }} + # uses: actions/setup-node@v1 + # with: + # node-version: ${{ matrix.node-version }} + # - run: npm install + # - name: Run karma tests + # run: npm run test-karma lint: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/.gitignore b/.gitignore index 2ae83b1..aaacd06 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,7 @@ node_modules/ jspm_packages/ # TypeScript v1 declaration files -types/ +#types/ # TypeScript cache *.tsbuildinfo diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf18c2..2312d23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # isomorphic-lib-template Changelog -## 1.0.0 - TBD +## 0.0.1 - TBD ### Added diff --git a/LICENSE.md b/LICENSE.md index 6d9b390..10ea4ba 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Digital Credentials Consortium +Copyright (c) 2025 Digital Credentials Consortium Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 51429f3..c81d616 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,900 @@ -# Example Isomorphic TS/JS Lib Template _(@digitalcredentials/isomorphic-lib-template)_ +# verifier-core _(@digitalcredentials/verifier-core)_ -[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/isomorphic-lib-template/main.yml?branch=main)](https://github.com/digitalcredentials/isomorphic-lib-template/actions?query=workflow%3A%22Node.js+CI%22) -[![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/isomorphic-lib-template.svg)](https://npm.im/@digitalcredentials/isomorphic-lib-template) +[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/verifier-core/main.yml?branch=jc-implement)](https://github.com/digitalcredentials/verifier-core/actions?query=workflow%3A%22Node.js+CI%22) +[![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/verifier-core.svg)](https://npm.im/@digitalcredentials/verifier-core) +[![Coverage Status](https://coveralls.io/repos/github/digitalcredentials/verifier-core/badge.svg?branch=jc-implement)](https://coveralls.io/github/digitalcredentials/verifier-core?branch=jc-implement) -> A Typescript/Javascript isomorphic library template, for use in the browser, Node.js, and React Native. +> Verifies W3C Verifiable Credentials in the browser, Node.js, and React Native. ## Table of Contents - -- [Background](#background) -- [Security](#security) +- [Overview](#overview) +- [API](#api) - [Install](#install) -- [Usage](#usage) - [Contribute](#contribute) - [License](#license) -## Background +## Overview + +Verifies the following versions of W3C Verifiable Credentials: + +* [1.0](https://www.w3.org/TR/2019/REC-vc-data-model-20191119/) +* [1.1](https://www.w3.org/TR/2022/REC-vc-data-model-20220303/) +* [2.0](https://www.w3.org/TR/vc-data-model-2.0/) + +And verifies signatures from both [eddsa-rdfc-2022 Data Integrity Proof](https://github.com/digitalbazaar/eddsa-rdfc-2022-cryptosuite) and [ed25519-signature-2020 Linked Data Proof](https://github.com/digitalbazaar/ed25519-signature-2020) cryptosuites. + +The verification checks that the credential: + +* has a valid signature, and so therefore: + * the credential hasn't been tampered with + * the public key was successfully retrieved from the did document +* hasn't expired +* hasn't been revoked +* was signed by a trusted issuer + +The verification will also tell us if any of the registries listed in the trusted registry list couldn't be loaded, which is important because those missing registries might be the very registries that affirm the trustworthiness of the issuer of a given credential. + +As of January 2025 issuers are trusted if they are listed in one of the Digital Credentials Issuer Registries: + +``` +{ + name: 'DCC Pilot Registry', + url: 'https://digitalcredentials.github.io/issuer-registry/registry.json' + }, + { + name: 'DCC Sandbox Registry', + url: 'https://digitalcredentials.github.io/sandbox-registry/registry.json' + }, + { + name: 'DCC Community Registry', + url: 'https://digitalcredentials.github.io/community-registry/registry.json' + }, + { + name: 'DCC Registry', + url: 'https://digitalcredentials.github.io/dcc-registry/registry.json' + } + ``` + + The DCC is working on a new trust registry model that will extend the registry scope. + +## API + +This package exports two methods: + +* verifyCredential +* verifyPresentation + +### verifyCredential + +```verifyCredential({credential, knownDidRegistries, reloadIssuerRegistry = true})``` + +#### arguments -TBD +* credential - The W3C Verifiable Credential to be verified. +* knownDidRegistries - a list of trusted registries. +* reloadIssuerRegistry - A boolean (true/false) indication whether or not to refresh the cached copy of the registries. + +#### result + +The typescript definitions for the result can be found [here](./src/types/result.ts) + +Note that the verification result doesn't make any conclusion about the overall validity of a credential. It only checks the validity of each of the four steps, leaving it up to the consumer of the result to decide on the overall validity. The consumer might not, for example, consider a credential that had expired or had been revoked to be 'invalid'. The credential might still in fact be useful as a record of history, i.e, I had a driver's licence that expired two years ago, but it was valid during the period 2018 to 2023, and that information might be useful. + +Four steps are checked, returning a result per step in a log like so: + + +``` +{ + "credential": {the VC that was submitted is returned here}, + "log": [ + { + "id": "valid_signature", + "valid": true (if it is false then an error is returned instead of the log) + }, + { + "id": "expiration", + "valid": true/false + }, + { + "id": "revocation_status", + "valid": true/false + }, + { + "id": "registered_issuer", + "valid": true/false, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded":[ + "DCC Issuer Registry" + ] + } + ] +} +``` + +Variations and errors are covered next... + +There are three general flavours of result that might be returned: + +1. all checks were conclusive + +All of the checks were run *conclusively*, meaning that we determined whether each of the four steps in verification (signature, expiry, revocation, known issuer) was true or false. + +A conclusive verification might look like this example where all steps returned valid=true: + +``` +{ + "credential": {the supplied vc - left out here for brevity/clarity}, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded":[] + } + ] +} +``` -## Security +Note that an invalid signature is considered fatal because it means that the revocation status, expiry data, or issuer id may have been tampered with, and so we can't say anything conclusive about any of them. -TBD +And here is a slightly different verification result where we have still made conclusive determinations about each step, and all are true except for the expiry: + +``` +{ + "credential": {the supplied vc - left out here for brevity/clarity}, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "expiration", + "valid": false + }, + "id": "revocation_status", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded":[] + } + ] +} +``` + +2. partially successful verification + +A verification might partly succeed if it can verify: + +* the signature +* the expiry date + +But can't retrieve (from the network) any one of: + +* the revocation status +* an issuer registry from our list of trusted issuers +* the issuer's DID document + +which are needed to verify the revocation status and issuer identity. + +For the valid_signature and revocation_status steps, if we can't conclusively verify one way or the other (true or false) we return an 'error' propery rather than a 'valid' property. + +For the registered_issuer step we always return false if the issuer isn't found in a loaded registry, but with the caveat that if the 'registriesNotLoaded' property does contain one or more registries, then the credential *might* have been in one of those registries. It is up to the consumer of the result to decide how to deal with that. + +A partially successful verification might look like this example, where we couldn't retrieve the status list or one of the registries: + +``` +{ + "credential": {the supplied vc - left out here for brevity/clarity}, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "revocation_status", + "error": { + "name": "'status_list_not_found'", + "message": "Could not retrieve the revocation status list." + } + }, + { + "id": "registered_issuer", + "valid": false, + "foundInRegistries": [], + "registriesNotLoaded": [ + { + "name": "DCC Sandbox Registry", + "url": "https://onlynoyrt.com/registry.json" + } + ] + } + ] +} +``` + +3. fatal error + +Fatal errors are errors that prevent us from saying anything conclusive about the credential, and so we don't list the results of each step (the 'log') because we can't decisively say if any are true or false. Reverting to saying they are all false would be misleading, because that could be interepreted to mean that the credential was, for example, revoked when really we just don't know one way or the other. + +Examples of fatal errors: + +invalid signature + +Fatal because if the signature is invalid it means any part of the credential could have been tampered with, including the revocation status, expiration, and issuer identity. In these cases we don't return a 'valid' property, but instead an 'errors' property + +``` +{ + "credential": {vc removed for brevity/clarity in this example}, + "errors": [ + { + "name": "invalid_signature", + "message": "The signature is not valid." + } + ] +} +``` + +unresolvable did + +Fatal because we couldn't retrieve the DID document containing the public signing key with which to check the signature. This error is most likely to happen with a did:web if the url for the did:web document is wrong or +has been taken down, or there is a network error. + +``` +{ + "credential": {vc removed for brevity/clarity}, + "errors": [ + { + "name": "did_web_unresolved", + "message": "The signature could not be checked because the public signing key could not be retrieved from https://digitalcredentials.github.io/dcc-did-web-bad/did.json" + } + ] +} +``` + +unknown http error + +A catchall error for unknown http errors when verifying the signature. + +``` +{ + "credential": {vc removed for brevity/clarity}, + "errors": [ + { + "name": "http_error_with_signature_check", + "message": "An http error prevented the signature check." + } + ] +} +``` + + + +malformed credential + +The supplied credential may not conform to the VerifiableCredential or LinkedData specifications(possibly because it follows some older convention, or maybe hasn't yet been signed) and might not even be a Verifiable Credential at all. + +Specific cases: + +invalid_jsonld + +There is no @context property at the top level of the credential: + +``` +{ + "credential": { + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:58:33Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" + } + }, + "errors": [ + { + "name": "invalid_jsonld", + "message": "The credential does not appear to be a valid jsonld document - there is no context." + } + ] +} +``` + +no_vc_context + +Although this is a linked data document, with an @context property, the Verifiable Credential context (i.e, "https://www.w3.org/2018/credentials/v1") is missing: + +``` +{ + "credential": { + "@context": [ + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:58:33Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" + } + }, + "errors": [ + { + "name": "no_vc_context", + "message": "The credential doesn't have a verifiable credential context." + } + ] +} +``` + +invalid_credential_id + +In this example, the top level id property on the credential is not a uri, but should be: + +``` +{ + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "0923lksjf", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": { + "type": [ + "Profile" + ], + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "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" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2023-10-05T11:17:41Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER" + } + }, + "errors": [ + { + "name": "invalid_credential_id", + "message": "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." + } + ] +} +``` + +no_proof + +The proof property is missing, likely because the credential hasn't been signed: + +``` +{ + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": { + "type": [ + "Profile" + ], + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "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" + } + }, + "errors": [ + { + "name": "no_proof", + "message": "This is not a Verifiable Credential - it does not have a digital signature." + } + ] +} +``` + + +other problem + +Some other error might also prevent verification, and an error, possibly with a stack trace, might be returned: + +``` +{ + "errors": [ + { + "name": "unknown_error", + "message": "Some kind of error - this message will depend on the error", + "stackTrace": "some kind of stack trace" + } + ] +} +``` + + +### verifyPresentation + +```verifyPresentation({presentation, reloadIssuerRegistry = true, unsignedPresentation = false})``` + +A Verifiable Presentation (VP) is a wrapper around zero or more Verifiable Credentials. A VP can be cryptographically signed, like a VC, but whereas a VC is signed by the issuer of the credentials, the VP is signed by the holder of the credentials contained in the VP, typically to demonstrate 'control' of the contained credentials. The VP is signed with a DID that the holder owns, and usually that DID was recorded inside the Verifiable Credentials - at the time of issuance - as the 'owner' or 'holder' of the credential. So by signing the VP with the private key corresponding to that DID we can prove we 'own' the credentials, or in other words, that the credentials were issued to us (to our DID.) + +A VP needn't be signed. It could simply be used as to 'package' together a set of VCs. + +A signed VP is also sometimes used for authentication, without any contained VC. Say for the case where when an issuer is issuing a credential to a DID, and the issuer wants to know that the recipient in fact does control that DID. In these cases the VP is typically the response to a request for [DIDAuthentication (DidAuth)](https://w3c-ccg.github.io/vp-request-spec/#did-authentication). This verifier-core library does not, however, provide verification for DidAuthentication, only to verify a presentation containing VCs. + +Verifying a VP amounts to verifying the signature on the VP (if the signature exists) and also verifying all of the contained VCs, one by one. + +#### arguments + +* presentation - The W3C Verifiable Presentation to be verified. +* reloadIssuerRegistry - Whether or not to refresh the cached copy of the registry. +* unsignedPresentation - wether the submitted vp has been signed or not + +#### result + +With a VP we have a result for the vp as well as for all the contained VCs. Each of the VC results follows exactly the format described above for the results of verifying an individual VCs. We may also have an error. + +A successful signed VP result with two packaged VCs might look like so: + +``` +{ + "presentationResult": { + "signature": "valid" + }, + "credentialResults": [ + { + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ], + "credential": {vc omitted for brevity/clarity} + }, + { + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ], + "credential": {vc omitted for brevity/clarity} + } + ] +} +``` + +A VP that itself verfies (i.e, it's signature), but has one VC that doesn't, might look like so: + +``` +{ + "presentationResult": { + "signature": "signed" + }, + "credentialResults": [ + { + "credential": { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "0923lksjf", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:58:33Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" + } + }, + "errors": [ + { + "name": "invalid_credential_id", + "message": "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." + } + ] + } + ] +} +``` + +It is important to note in the above example that the validity of the signature of the presentation is different from the validity of each of the contained VCs. A valid presentation signature simply means that nothing in the VP was tampered with. + +An unsigned VP containing a single verified credential: + +``` +{ + "presentationResult": { + "signature": "unsigned" + }, + "credentialResults": [ + { + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "revocation_status", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ], + "credential": {vc omitted for brevity/clarity} + } + ] +} +``` + +A VP where we've tampered with one of the packaged credentials (by changing the credential name). Note here that both the VP and the VC don't verify because changing the VC affected the VC's signature and also the VP signature which contains the VC. + +``` +{ + "presentationResult": { + "signature": "invalid", + "errors": [ + { + "message": { + "name": "VerificationError", + "errors": [ + { + "name": "Error", + "message": "Invalid signature.", + "stack": "Error: Invalid signature.\n at Ed25519Signature2020.verifyProof (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/suites/LinkedDataSignature.js:189:15)\n at async /Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:273:53\n at async Promise.all (index 0)\n at async _verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:261:3)\n at async ProofSet.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:195:23)\n at async Object.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/jsonld-signatures.js:160:18)\n at async _verifyPresentation (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/vc/dist/index.js:578:30)\n at async verifyPresentation (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/src/Verify.js:24:24)\n at async Context. (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/test/Verify.presentation.spec.js:97:28)" + } + ] + }, + "name": "presentation_error" + } + ] + }, + "credentialResults": [ + { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Tampered Name", + "issuer": { + "type": [ + "Profile" + ], + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "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" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2023-10-05T11:17:41Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER" + } + }, + "errors": [ + { + "name": "invalid_signature", + "message": "The signature is not valid." + } + ] + } + ] +} +``` + +And here is a VP where just the VP has been tampered with, and not the embedded VC, and so the VC returns as valid, but not the presentation signature: + +``` +{ + "presentationResult": { + "signature": "invalid", + "errors": [ + { + "name": "presentation_error", + "message": { + "name": "VerificationError", + "errors": [ + { + "name": "Error", + "message": "Invalid signature.", + "stack": "Error: Invalid signature.\n at Ed25519Signature2020.verifyProof (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/suites/LinkedDataSignature.js:189:15)\n at async /Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:273:53\n at async Promise.all (index 0)\n at async _verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:261:3)\n at async ProofSet.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/ProofSet.js:195:23)\n at async Object.verify (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/jsonld-signatures/lib/jsonld-signatures.js:160:18)\n at async _verifyPresentation (/Users/jameschartrand/Documents/github/dcc/verifier-core/node_modules/@digitalcredentials/vc/dist/index.js:578:30)\n at async verifyPresentation (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/src/Verify.js:24:24)\n at async Context. (file:///Users/jameschartrand/Documents/github/dcc/verifier-core/dist/test/Verify.presentation.spec.js:101:28)" + } + ] + } + } + ] + }, + "credentialResults": [ + { + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ], + "credential": {vc ommitted for clarity/brevity} + } + ] +} +``` ## Install -- Node.js 16+ is recommended. +- Node.js 20+ is recommended. ### NPM To install via NPM: ``` -npm install @digitalcredentials/isomorphic-lib-template +npm install @digitalcredentials/verifier-core ``` ### Development @@ -39,22 +902,15 @@ npm install @digitalcredentials/isomorphic-lib-template To install locally (for development): ``` -git clone https://github.com/digitalcredentials/isomorphic-lib-template.git -cd isomorphic-lib-template +git clone https://github.com/digitalcredentials/verifier-core.git +cd verifier-core npm install ``` -## Usage - -TBD - ## Contribute PRs accepted. -If editing the Readme, please conform to the -[standard-readme](https://github.com/RichardLitt/standard-readme) specification. - ## License -[MIT License](LICENSE.md) © 2022 Digital Credentials Consortium. +[MIT License](LICENSE.md) © 2025 Digital Credentials Consortium. diff --git a/karma.conf.js b/karma.conf.cjs similarity index 100% rename from karma.conf.js rename to karma.conf.cjs diff --git a/package.json b/package.json index 6dab6b5..a1a6d22 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { - "name": "@digitalcredentials/isomorphic-lib-template", - "description": "A Typescript/Javascript isomorphic library template, for use in the browser, Node.js, and React Native.", - "version": "0.0.1", + "name": "@digitalcredentials/verifier-core", + "description": "For verifying Verifiable Credentials in the browser, Node.js, and React Native.", + "version": "1.0.0", "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", + "build-test": "npm run clear && tsc -p tsconfig.spec.json", "clear": "rimraf dist/*", "lint": "eslint .", "lint:fix": "eslint --fix .", @@ -11,27 +12,42 @@ "prettier": "prettier src --write", "rebuild": "npm run clear && npm run build", "test": "npm run lint && npm run test-node", - "test-karma": "karma start karma.conf.js", - "test-node": "cross-env NODE_ENV=test TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --project tsconfig.spec.json 'test/*.ts'" + "test-karma": "karma start karma.conf.cjs", + "test-node-no-cov": "npm run build-test && mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", + "test-node": "npm run build-test && npx c8 --exclude 'dist/test/**' mocha dist/test/*.spec.js && rm -rf dist/esm/test || true", + "coveralls": "npm run test; npx c8 --exclude 'dist/test/**' report --reporter=text-lcov > ./coverage/lcov.info" + }, + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js" + }, + "./package.json": "./package.json" }, "files": [ - "dist", - "README.md", - "LICENSE.md" + "dist/**/*.js" ], - "main": "dist/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", "dependencies": { + "@digitalcredentials/data-integrity": "^2.5.1-beta.1", + "@digitalcredentials/ed25519-signature-2020": "^6.0.0", + "@digitalcredentials/eddsa-rdfc-2022-cryptosuite": "^1.2.1-beta.6", + "@digitalcredentials/issuer-registry-client": "^3.0.1-beta.1", + "@digitalcredentials/jsonld-signatures": "^12.0.1", + "@digitalcredentials/security-document-loader": "^6.0.1", + "@digitalcredentials/vc": "^9.0.1-beta.2", + "@digitalcredentials/vc-bitstring-status-list": "^1.0.0" }, "devDependencies": { "@types/chai": "^4.3.4", + "@types/deep-equal-in-any-order": "^1.0.4", "@types/mocha": "^10.0.1", "@types/node": "^18.11.17", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "chai": "^4.3.7", + "coveralls": "^3.1.1", "cross-env": "^7.0.3", + "deep-equal-in-any-order": "^2.0.6", "eslint": "^8.30.0", "eslint-config-prettier": "^8.5.0", "eslint-config-standard-with-typescript": "^24.0.0", @@ -50,23 +66,17 @@ "prettier": "^2.8.1", "rimraf": "^3.0.2", "ts-node": "^10.9.1", + "tsx": "^4.19.2", "typescript": "^4.9.4" }, "publishConfig": { "access": "public" }, - "mocha": { - "require": "ts-node/register", - "extension": [ - "ts" - ], - "spec": "test/**/*.ts" - }, "keywords": [ "dcc" ], "engines": { - "node": ">=16.0" + "node": ">=18.0" }, "author": { "name": "Digital Credentials Consortium", @@ -75,8 +85,8 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/digitalcredentials/isomorphic-lib-template" + "url": "https://github.com/digitalcredentials/verifier-core" }, - "homepage": "https://github.com/digitalcredentials/isomorphic-lib-template", - "bugs": "https://github.com/digitalcredentials/isomorphic-lib-template/issues" + "homepage": "https://github.com/digitalcredentials/verifier-core", + "bugs": "https://github.com/digitalcredentials/verifier-core" } diff --git a/src/Example.ts b/src/Example.ts deleted file mode 100644 index 4e4a5f1..0000000 --- a/src/Example.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * Copyright (c) 2022 Digital Credentials Consortium. All rights reserved. - */ -export class Example { - public hello(): string { - return 'world' - } -} diff --git a/src/Verify.ts b/src/Verify.ts new file mode 100644 index 0000000..ad52736 --- /dev/null +++ b/src/Verify.ts @@ -0,0 +1,244 @@ +// import '@digitalcredentials/data-integrity-rn'; +import { Ed25519Signature2020 } from '@digitalcredentials/ed25519-signature-2020'; +import { DataIntegrityProof } from '@digitalcredentials/data-integrity'; +import { cryptosuite as eddsaRdfc2022CryptoSuite } from '@digitalcredentials/eddsa-rdfc-2022-cryptosuite'; +import * as vc from '@digitalcredentials/vc'; +import { securityLoader } from '@digitalcredentials/security-document-loader'; +import pkg from '@digitalcredentials/jsonld-signatures'; + +import { getCredentialStatusChecker } from './credentialStatus.js'; +import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js'; +import { extractCredentialsFrom } from './extractCredentialsFrom.js'; + +import { + PRESENTATION_ERROR, UNKNOWN_ERROR, INVALID_JSONLD, NO_VC_CONTEXT, + INVALID_CREDENTIAL_ID, NO_PROOF, STATUS_LIST_NOT_FOUND, + HTTP_ERROR_WITH_SIGNATURE_CHECK, DID_WEB_UNRESOLVED, + INVALID_SIGNATURE } from './constants/errors.js'; +import { SIGNATURE_INVALID, SIGNATURE_VALID, SIGNATURE_UNSIGNED, REVOCATION_STATUS_STEP_ID } from './constants/verificationSteps.js'; +import { ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, VERIFICATION_ERROR } from './constants/external.js'; + +import { Credential } from './types/credential.js'; +import { VerificationResponse, VerificationStep, PresentationVerificationResponse, PresentationSignatureResult } from './types/result.js'; +import { VerifiablePresentation } from './types/presentation.js'; + +const { purposes } = pkg; +const presentationPurpose = new purposes.AssertionProofPurpose(); + +const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); + +// for verifying eddsa-2022 signatures +const eddsaSuite = new DataIntegrityProof({ cryptosuite: eddsaRdfc2022CryptoSuite }); +// for verifying ed25519-2020 signatures +const ed25519Suite = new Ed25519Signature2020(); +// add both suites - the vc lib will use whichever is appropriate +const suite = [ed25519Suite, eddsaSuite] + +export async function verifyPresentation({ presentation, challenge = 'meaningless', unsignedPresentation = false, knownDIDRegistries, reloadIssuerRegistry = true }: + { + presentation: VerifiablePresentation, + challenge?: string | null, + unsignedPresentation?: boolean, + knownDIDRegistries: object, + reloadIssuerRegistry?: boolean + } +): Promise { + try { + const credential = extractCredentialsFrom(presentation)?.find( + vc => vc.credentialStatus); + const checkStatus = credential ? getCredentialStatusChecker(credential) : undefined; + const result = await vc.verify({ + presentation, + presentationPurpose, + suite, + documentLoader, + unsignedPresentation, + checkStatus, + challenge, + verifyMatchingIssuers: false + }); + + const transformedCredentialResults = await Promise.all(result.credentialResults.map(async (credentialResult: any) => { + return transformResponse(credentialResult, credentialResult.credential, knownDIDRegistries, reloadIssuerRegistry) + })); + + // take what we need from the presentation part of the result + let signature: PresentationSignatureResult; + if (unsignedPresentation) { + signature = SIGNATURE_UNSIGNED + } else { + signature = result.presentationResult.verified ? SIGNATURE_VALID : SIGNATURE_INVALID + } + const errors = result.error ? [{ message: result.error, name: PRESENTATION_ERROR }] : null + const presentationResult = { signature, ...(errors && { errors }) } + + return { presentationResult, credentialResults: transformedCredentialResults }; + } catch (error) { + return { errors: [{ message: 'Could not verify presentation.', name: PRESENTATION_ERROR, stackTrace: error }] } + } +} + + +export async function verifyCredential({ credential, knownDIDRegistries, reloadIssuerRegistry = true }: { credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean }): Promise { + try { + // null unless credential has a status + const statusChecker = getCredentialStatusChecker(credential) + + const verificationResponse = await vc.verifyCredential({ + credential, + suite, + documentLoader, + checkStatus: statusChecker, + verifyMatchingIssuers: false + }); + + const adjustedResponse = await transformResponse(verificationResponse, credential, knownDIDRegistries, reloadIssuerRegistry) + return adjustedResponse; + } catch (error) { + return { errors: [{ message: 'Could not verify credential.', name: UNKNOWN_ERROR, stackTrace: error }] } + } +} + +async function transformResponse(verificationResponse: any, credential: Credential, knownDIDRegistries: object, reloadIssuerRegistry: boolean): Promise { + + const fatalCredentialError = handleAnyFatalCredentialErrors(credential) + + if (fatalCredentialError) { + return fatalCredentialError + } + + handleAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult }); + + const fatalSignatureError = handleAnySignatureError({ verificationResponse, credential }) + if (fatalSignatureError) { + return fatalSignatureError + } + + const { issuer } = credential + await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, reloadIssuerRegistry, issuer }) + + // remove things we don't need from the result or that are duplicated elsewhere + delete verificationResponse.results + delete verificationResponse.statusResult + delete verificationResponse.verified + delete verificationResponse.credentialId + verificationResponse.log = verificationResponse.log.filter((entry: VerificationStep) => entry.id !== ISSUER_DID_RESOLVES) + + // add things we always want in the response + verificationResponse.credential = credential + + return verificationResponse as VerificationResponse; +} + +function buildFatalErrorObject(fatalErrorMessage: string, name: string, credential: Credential, stackTrace: string | null): VerificationResponse { + return { credential, errors: [{ name, message: fatalErrorMessage, ...(stackTrace ? { stackTrace } : null) }] }; +} + +function handleAnyFatalCredentialErrors(credential: Credential): VerificationResponse | null { + const validVCContexts = [ + 'https://www.w3.org/2018/credentials/v1', + 'https://www.w3.org/ns/credentials/v2' + ] + const suppliedContexts = credential['@context'] + + if (!suppliedContexts) { + const fatalErrorMessage = "The credential does not appear to be a valid jsonld document - there is no context." + const name = INVALID_JSONLD + return buildFatalErrorObject(fatalErrorMessage, name, credential, null) + } + + if (!validVCContexts.some(contextURI => suppliedContexts.includes(contextURI))) { + const fatalErrorMessage = "The credential doesn't have a verifiable credential context." + const name = NO_VC_CONTEXT + return buildFatalErrorObject(fatalErrorMessage, name, credential, null) + } + + try { + // eslint-disable-next-line no-new + new URL(credential.id as string); + } catch (e) { + const fatalErrorMessage = "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement." + const name = INVALID_CREDENTIAL_ID + return buildFatalErrorObject(fatalErrorMessage, name, credential, null) + } + + if (!credential.proof) { + const fatalErrorMessage = 'This is not a Verifiable Credential - it does not have a digital signature.' + const name = NO_PROOF + return buildFatalErrorObject(fatalErrorMessage, name, credential, null) + } + + return null +} + +function handleAnyStatusError({ verificationResponse }: { + verificationResponse: any, + statusResult: any +}): void { + const statusResult = verificationResponse.statusResult + if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) { + const statusStep = { + "id": REVOCATION_STATUS_STEP_ID, + "error": { + name: STATUS_LIST_NOT_FOUND, + message: statusResult.error.cause.message + } + }; + (verificationResponse.log ??= []).push(statusStep) + } +} + +function handleAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }): null | VerificationResponse { + if (verificationResponse.error) { + + if (verificationResponse?.error?.name === VERIFICATION_ERROR) { + // Can't validate the signature. + // Either a bad signature or maybe a did:web that can't + // be resolved. Because we can't validate the signature, we + // can't therefore say anything conclusive about the various + // steps in verification. + // So, return a fatal error and no log (because we can't say + // anything meaningful about the steps in the log) + let fatalErrorMessage = "" + let errorName = "" + // check to see if the error is http related + const httpError = verificationResponse.error.errors.find((error: any) => error.name === 'HTTPError') + if (httpError) { + fatalErrorMessage = 'An http error prevented the signature check.' + errorName = HTTP_ERROR_WITH_SIGNATURE_CHECK + // was it caused by a did:web that couldn't be resolved??? + const issuerDID: string = (((credential.issuer) as any).id) || credential.issuer + if (issuerDID.toLowerCase().startsWith('did:web')) { + // change did to a url: + const didUrl = issuerDID.slice(8).replaceAll(':', '/').toLowerCase() + if (httpError.requestUrl.toLowerCase().includes(didUrl)) { + fatalErrorMessage = `The signature could not be checked because the public signing key could not be retrieved from ${httpError.requestUrl as string}` + errorName = DID_WEB_UNRESOLVED + } + } + } else { + // not an http error, so likely bad signature + fatalErrorMessage = 'The signature is not valid.' + errorName = INVALID_SIGNATURE + } + const stackTrace = verificationResponse?.error?.errors?.stack + return buildFatalErrorObject(fatalErrorMessage, errorName, credential, stackTrace) + + + } else if (verificationResponse.error.log) { + // There wasn't actually an error, it is just that one of the + // steps returned false. + // So move the log out of the error to the response, since it + // isn't part of the error + verificationResponse.log = verificationResponse.error.log + // delete the error, because again, this wasn't an error, just + // a false value on one of the steps + delete verificationResponse.error + } + } + return null +} + + + + diff --git a/src/constants/errors.ts b/src/constants/errors.ts new file mode 100644 index 0000000..615f289 --- /dev/null +++ b/src/constants/errors.ts @@ -0,0 +1,10 @@ +export const PRESENTATION_ERROR = 'presentation_error' +export const UNKNOWN_ERROR = 'unknown_error' +export const INVALID_JSONLD = 'invalid_jsonld' +export const NO_VC_CONTEXT = 'no_vc_context' +export const INVALID_CREDENTIAL_ID = 'invalid_credential_id' +export const NO_PROOF = 'no_proof' +export const STATUS_LIST_NOT_FOUND = 'status_list_not_found' +export const HTTP_ERROR_WITH_SIGNATURE_CHECK = 'http_error_with_signature_check' +export const DID_WEB_UNRESOLVED = 'did_web_unresolved' +export const INVALID_SIGNATURE = 'invalid_signature' diff --git a/src/constants/external.ts b/src/constants/external.ts new file mode 100644 index 0000000..7476b46 --- /dev/null +++ b/src/constants/external.ts @@ -0,0 +1,4 @@ +// constants used by @digitalcredentials.vc library +export const ISSUER_DID_RESOLVES = 'issuer_did_resolves' +export const NOT_FOUND_ERROR = 'NotFoundError' +export const VERIFICATION_ERROR = 'VerificationError' \ No newline at end of file diff --git a/src/constants/verificationSteps.ts b/src/constants/verificationSteps.ts new file mode 100644 index 0000000..6f868a0 --- /dev/null +++ b/src/constants/verificationSteps.ts @@ -0,0 +1,9 @@ +// VERIFICATION STEP iDS +export const REVOCATION_STATUS_STEP_ID = 'revocation_status' +export const REGISTERED_ISSUER_STEP_ID = 'registered_issuer' +export const EXPIRATION_STEP_ID = 'expiration' +export const VALID_SIGNATURE_STEP_ID = 'valid_signature' +// VERIFICATION VALUES +export const SIGNATURE_VALID = 'valid' +export const SIGNATURE_INVALID = 'invalid' +export const SIGNATURE_UNSIGNED = 'unsigned' diff --git a/src/credentialStatus.ts b/src/credentialStatus.ts new file mode 100644 index 0000000..760f6f1 --- /dev/null +++ b/src/credentialStatus.ts @@ -0,0 +1,24 @@ +import { checkStatus } from '@digitalcredentials/vc-bitstring-status-list'; +import { Credential } from './types/credential'; + +export enum StatusPurpose { + Revocation = 'revocation', + Suspension = 'suspension' +} + +export function getCredentialStatusChecker(credential: Credential) : (() => boolean) | null { + let statusChecker = null; + if (!credential.credentialStatus) { + return null; + } + const credentialStatuses = Array.isArray(credential.credentialStatus) ? + credential.credentialStatus : + [credential.credentialStatus]; + const [credentialStatus] = credentialStatuses; + if (credentialStatus.type === 'BitstringStatusListEntry') { + statusChecker = checkStatus; + } + return statusChecker; + +} + diff --git a/src/declarations.d.ts b/src/declarations.d.ts index d4dea51..9509cb8 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -1 +1,11 @@ -// declare module 'jsonld' +declare module '@digitalcredentials/did-io'; +declare module '@digitalcredentials/did-method-key'; +declare module '@digitalcredentials/vc'; +declare module '@digitalcredentials/vc-bitstring-status-list'; +declare module '@digitalcredentials/vc-status-list'; +declare module '@digitalcredentials/vpqr'; +declare module '@digitalcredentials/jsonld-signatures'; +declare module '@digitalcredentials/data-integrity'; +declare module '@digitalcredentials/eddsa-rdfc-2022-cryptosuite'; +declare module '@digitalcredentials/ed25519-signature-2020'; +declare module '@digitalcredentials/ed25519-verification-key-2020'; \ No newline at end of file diff --git a/src/extractCredentialsFrom.ts b/src/extractCredentialsFrom.ts new file mode 100644 index 0000000..ef5322e --- /dev/null +++ b/src/extractCredentialsFrom.ts @@ -0,0 +1,10 @@ +import { Credential } from './types/credential.js'; +import { VerifiablePresentation } from './types/presentation'; + +export function extractCredentialsFrom(vp: VerifiablePresentation): Credential[] | null { + const { verifiableCredential } = vp; + if (verifiableCredential instanceof Array) { + return verifiableCredential; + } + return [verifiableCredential]; +} diff --git a/src/index.ts b/src/index.ts index 86d30cb..1bd0438 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ /*! * Copyright (c) 2022 Digital Credentials Consortium. All rights reserved. */ -export { Example } from './Example' +export { verifyCredential, // verifyPresentation + } from './Verify.js' diff --git a/src/issuerRegistries.ts b/src/issuerRegistries.ts new file mode 100644 index 0000000..4259d92 --- /dev/null +++ b/src/issuerRegistries.ts @@ -0,0 +1,57 @@ +import {RegistryClient, LoadResult} from '@digitalcredentials/issuer-registry-client'; +import { VerificationResponse, RegistryListResult } from './types/result.js'; +import { REGISTERED_ISSUER_STEP_ID } from './constants/verificationSteps.js'; +const registries = new RegistryClient() +const registryNotYetLoaded = true; + +/** + * Checks to see if a VC's issuer appears in any of the known DID registries. + * + * @returns An object containing a list of the names of the DID registries in + * which the issuer appears and a list of registries that couldn't be loaded + */ + +export async function getTrustedRegistryListForIssuer({ issuer, knownDIDRegistries, reloadIssuerRegistry = false }: { + issuer: string | any, + knownDIDRegistries: object, + reloadIssuerRegistry: boolean | null +}): Promise { + + let registryLoadResult:LoadResult[] = [] + // eslint-disable-next-line no-use-before-define + if (reloadIssuerRegistry || registryNotYetLoaded) { + registryLoadResult = await registries.load({ config: knownDIDRegistries }) + } + const registriesNotLoaded : Array<{name: string, url: string}> = registryLoadResult.filter((registry:LoadResult)=>!registry.loaded).map(entry=>{return {name:entry.name, url:entry.url}}) + const issuerDid = typeof issuer === 'string' ? issuer : issuer.id; + const issuerInfo = registries.didEntry(issuerDid); + // See if the issuer DID appears in any of the known registries + // If yes, assemble a list of registries in which it appears + const foundInRegistries = issuerInfo?.inRegistries + ? Array.from(issuerInfo.inRegistries).map(r => r.name) + : [] + + return {foundInRegistries, registriesNotLoaded} + +} + +export async function addTrustedIssuersToVerificationResponse( {issuer, knownDIDRegistries, reloadIssuerRegistry = false, verificationResponse} :{ + issuer: string | any, + reloadIssuerRegistry: boolean | null, + knownDIDRegistries: object, + verificationResponse: VerificationResponse +}) : Promise + { + const {foundInRegistries,registriesNotLoaded} = await getTrustedRegistryListForIssuer( {issuer, knownDIDRegistries, reloadIssuerRegistry}); + + const registryStep = { + "id": REGISTERED_ISSUER_STEP_ID, + "valid": !!foundInRegistries.length, + foundInRegistries, + registriesNotLoaded + }; + + (verificationResponse.log ??= []).push(registryStep) + +} + diff --git a/src/test-fixtures/expectedResults.ts b/src/test-fixtures/expectedResults.ts new file mode 100644 index 0000000..8621073 --- /dev/null +++ b/src/test-fixtures/expectedResults.ts @@ -0,0 +1,97 @@ +import { VerificationResponse, VerificationStep, PresentationVerificationResponse } from "src/types/result"; + +const expectedPresentationResult = { + "presentationResult": { + "signature": 'valid', + } +} +const expectedResult = { + "credential": {}, + "log": [ + { + "id": "valid_signature", + "valid": true + }, + { + "id": "expiration", + "valid": true + }, + { + "id": "registered_issuer", + "valid": true, + "foundInRegistries": [ + "DCC Sandbox Registry" + ], + "registriesNotLoaded": [] + } + ] + } + + const fatalResult = { + credential: {}, + errors: [ + { + name: 'error name goes here, e.g., no_proof', + message: 'error message goes here' + } + ] + } + + const getCopyOfFatalResult = (credential:object, errorName:string, errorMessage:string) : VerificationResponse => { + const expectedResultCopy = JSON.parse(JSON.stringify(fatalResult)) + expectedResultCopy.credential = credential; + expectedResultCopy.errors[0].name = errorName; + expectedResultCopy.errors[0].message = errorMessage + return expectedResultCopy; + } + + const getCopyOfExpectedResult = (credential:object, withStatus: boolean) : VerificationResponse => { + const expectedResultCopy = JSON.parse(JSON.stringify(expectedResult)) + if (withStatus) { + expectedResultCopy.log?.push( + { + "id": "revocation_status", + "valid": true + } + ) + } + expectedResultCopy.credential = credential; + return expectedResultCopy; + } + + const getCopyOfExpectedVPResult = () : PresentationVerificationResponse => { + return JSON.parse(JSON.stringify(expectedPresentationResult)) + } + + + const getExpectedVerifiedResult = ({credential, withStatus }: {credential:object, withStatus:boolean}) : VerificationResponse => { + return getCopyOfExpectedResult(credential, withStatus); + } + + const getExpectedUnverifiedResult = ( {credential, unVerifiedStep, withStatus }: {credential:object, unVerifiedStep:string, withStatus:boolean}) : VerificationResponse => { + const expectedResult = getCopyOfExpectedResult(credential, withStatus); + const step = expectedResult.log?.find((entry:VerificationStep)=>entry.id === unVerifiedStep) + if (step) step.valid = false; + return expectedResult; + } + + const getExpectedFatalResult = ( {credential, errorName, errorMessage }: {credential:object, errorName:string, errorMessage:string}) : VerificationResponse => { + const expectedResult = getCopyOfFatalResult(credential, errorName, errorMessage); + return expectedResult; + } + + const getExpectedVerifiedPresentationResult = ({credentialResults, unsigned = false}: {credentialResults:VerificationResponse[], unsigned?:boolean}) : PresentationVerificationResponse => { + const expectedResult = getCopyOfExpectedVPResult(); + expectedResult.credentialResults = credentialResults + if (unsigned && expectedResult.presentationResult) { + expectedResult.presentationResult.signature = 'unsigned' + } + return expectedResult; + } + + export { + getExpectedVerifiedResult, + getExpectedUnverifiedResult, + getExpectedFatalResult, + getExpectedVerifiedPresentationResult + } \ No newline at end of file diff --git a/src/test-fixtures/knownDidRegistries.ts b/src/test-fixtures/knownDidRegistries.ts new file mode 100644 index 0000000..515f591 --- /dev/null +++ b/src/test-fixtures/knownDidRegistries.ts @@ -0,0 +1,18 @@ +export const knownDIDRegistries : object = [ + { + name: 'DCC Pilot Registry', + url: 'https://digitalcredentials.github.io/issuer-registry/registry.json' + }, + { + name: 'DCC Sandbox Registry', + url: 'https://digitalcredentials.github.io/sandbox-registry/registry.json' + }, + { + name: 'DCC Community Registry', + url: 'https://digitalcredentials.github.io/community-registry/registry.json' + }, + { + name: 'DCC Registry', + url: 'https://digitalcredentials.github.io/dcc-registry/registry.json' + } + ] \ No newline at end of file diff --git a/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj b/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj new file mode 100644 index 0000000..d4758e5 --- /dev/null +++ b/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj @@ -0,0 +1,26 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj", + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ], + "credentialSubject": { + "id": "https://testing.dcconsortium.org/status/e5WK8CbZ1GjycuPombrj#list", + "type": "BitstringStatusList", + "encodedList": "uH4sIAAAAAAAAA-3BMQEAAAwCoGUx6aLbwgvIHwAAAAAAAAAAAAAAwFwBZnztF9QwAAA", + "statusPurpose": "revocation" + }, + "issuer": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "validFrom": "2025-01-09T15:20:02.183Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T15:20:02Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z4WFodWdHXGieqNtWYK2448A7qZdhMkxyqjVuMqifdanFYXXAqPT8xatjncxjDsXT6fskz8pC8TLBmEhnd7BC7Tqb" + } +} \ No newline at end of file diff --git a/src/test-fixtures/vc.ts b/src/test-fixtures/vc.ts new file mode 100644 index 0000000..3dab2b7 --- /dev/null +++ b/src/test-fixtures/vc.ts @@ -0,0 +1,171 @@ +import { v2NoStatus } from "./verifiableCredentials/v2/v2NoStatus.js" +import { v2Expired } from "./verifiableCredentials/v2/v2Expired.js" +import { v2Revoked } from "./verifiableCredentials/v2/v2Revoked.js" +import { v2WithValidStatus } from "./verifiableCredentials/v2/v2WithValidStatus.js" +import { v2ExpiredWithValidStatus } from "./verifiableCredentials/v2/v2ExpiredWithValidStatus.js" + +import { v1WithValidStatus } from "./verifiableCredentials/v1/v1WithValidStatus.js" +import { v1NoStatus } from "./verifiableCredentials/v1/v1NoStatus.js" +import { v1Revoked } from "./verifiableCredentials/v1/v1Revoked.js" +import { v1Expired } from "./verifiableCredentials/v1/v1Expired.js" +import { v1ExpiredWithValidStatus } from "./verifiableCredentials/v1/v1ExpiredWithValidStatus.js" +import { v2SimpleIssuerId } from "./verifiableCredentials/v2/v2SimpleIssuerId.js" +import { v2EddsaWithValidStatus } from "./verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.js" +import { v2DoubleSigWithBadStatusUrl } from "./verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.js" + +import { v2didWebWithValidStatus } from "./verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.js" +import { v2WithBadDidWeb } from "./verifiableCredentials/v2/didWeb/v2WithBadDidWeb.js" +import { v1SimpleIssuerId } from "./verifiableCredentials/v1/v1SimpleIssuerId.js" + +const getVCv1 = (): any => { + return JSON.parse(JSON.stringify(v1NoStatus)) +} + +const getVCv2 = (): any => { + return JSON.parse(JSON.stringify(v2NoStatus)) +} + +const getVCv2NoProof = (): any => { + const v2 = getVCv2() + delete v2.proof + return v2 + } + +const getVCv1NoProof = (): any => { + const v1 = getVCv1() + delete v1.proof + return v1 +} + + + +const getVCv1ValidStatus = (): any => { + return v1WithValidStatus +} +const getVCv2ValidStatus = (): any => { + return v2WithValidStatus +} + +const getVCv1Tampered = (): any => { + const signedVC1 = getVCv1() + signedVC1.name = 'Introduction to Tampering' + return signedVC1 +} + +const getVCv1Expired = (): any => { + return JSON.parse(JSON.stringify(v1Expired)) +} +const getVCv2Expired = (): any => { + return JSON.parse(JSON.stringify(v2Expired)) +} +const getVCv2Revoked = (): any => { + return JSON.parse(JSON.stringify(v2Revoked)) +} +const getVCv1Revoked = (): any => { + return JSON.parse(JSON.stringify(v1Revoked)) +} + +const getVCv2ExpiredAndTampered = (): any => { + const cred = getVCv2Expired() + cred.name = 'tampered!' + return cred +} +const getVCv1ExpiredAndTampered = (): any => { + const cred = getVCv1Expired() + cred.name = 'tampered!' + return cred +} +const getVCv2Tampered = (): any => { + const cred = getVCv2() + cred.name = 'tampered!' + return cred +} + +const getCredentialWithoutContext = (): any => { + const cred = getVCv2() + delete cred['@context'] + return cred +} + +const getCredentialWithoutVCContext = (): any => { + const cred = getVCv2() + cred['@context'] = cred['@context'].filter((context : string) => context !== 'https://www.w3.org/ns/credentials/v2') // remove the vc context + return cred +} + +const getVCv2NonURIId = (): any => { + const cred = getVCv2() + cred.id = "0923lksjf" + return cred +} + +const getVCv1NonURIId = (): any => { + const cred = getVCv1() + cred.id = "0923lksjf" + return cred +} + +const getVCv1ExpiredWithValidStatus = (): any => { + return JSON.parse(JSON.stringify(v1ExpiredWithValidStatus)) +} + +const getVCv2ExpiredWithValidStatus = (): any => { + return JSON.parse(JSON.stringify(v2ExpiredWithValidStatus)) +} + +const getVCv2EddsaWithValidStatus = (): any => { + return JSON.parse(JSON.stringify(v2EddsaWithValidStatus)) +} + +const getVCv2DoubleSigWithBadStatusUrl = (): any => { + return JSON.parse(JSON.stringify(v2DoubleSigWithBadStatusUrl)) +} + +const getVCv2DidWebWithValidStatus = (): any => { + return JSON.parse(JSON.stringify(v2didWebWithValidStatus)) +} + +const getVCv2WithBadDidWebUrl = (): any => { + return JSON.parse(JSON.stringify(v2WithBadDidWeb)) +} + +const getVCv1SimpleIssuerId = (): any => { + return JSON.parse(JSON.stringify(v1SimpleIssuerId)) +} + +const getVCv2SimpleIssuerId = (): any => { + return JSON.parse(JSON.stringify(v2SimpleIssuerId)) +} + +export { + + getCredentialWithoutContext, + getCredentialWithoutVCContext, + + getVCv2EddsaWithValidStatus, + getVCv2DoubleSigWithBadStatusUrl, + + getVCv2, + getVCv2SimpleIssuerId, + getVCv2Expired, + getVCv2Revoked, + getVCv2Tampered, + getVCv2ValidStatus, + getVCv2ExpiredAndTampered, + getVCv2ExpiredWithValidStatus, + getVCv2NoProof, + getVCv2NonURIId, + getVCv2DidWebWithValidStatus, + getVCv2WithBadDidWebUrl, + + getVCv1, + getVCv1SimpleIssuerId, + getVCv1Expired, + getVCv1Revoked, + getVCv1Tampered, + getVCv1ValidStatus, + getVCv1ExpiredAndTampered, + getVCv1ExpiredWithValidStatus, + getVCv1NoProof, + getVCv1NonURIId +} diff --git a/src/test-fixtures/verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.ts new file mode 100644 index 0000000..665d926 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/eddsa/v2/v2EddsaWithValidStatus.ts @@ -0,0 +1,53 @@ +export const v2EddsaWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": { + "type": "DataIntegrityProof", + "created": "2025-01-15T17:08:56Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z2gSJFxCToKUpGAf4DfjvVPjW6F9ktz6UgCAg7siW5usS3GCbiWSuMaiExapnDox2YLvEtcPyx7ZHMPmfDexJYXKm" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.ts b/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.ts new file mode 100644 index 0000000..f5325b5 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithBadStatusUrl.ts @@ -0,0 +1,62 @@ +export const v2DoubleSigWithBadStatusUrl = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2025-01-19T20:13:44Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5fqVd2F99EP5yDuUXmGpDvQS7tkz981ZL4qYkHv6ienYmeQy5za5a2qjgZfQWu65DPD9vSxyjgqACt1sHSmEyq9b" + }, + { + "type": "DataIntegrityProof", + "created": "2025-01-19T20:13:44Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z4ybSpzWGcrs8HrZ8KdiH2CHAetsq6e926bi8KmEfWPMsWPGbfpZDxgKSaGcCmCwmD92qEpRrpr9bM476BhfbhkDg" + } + ] +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithValidStatus.ts new file mode 100644 index 0000000..79752c9 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/eddsaAndEd25519/v2/v2DoubleSigWithValidStatus.ts @@ -0,0 +1,62 @@ +export const v2DoubleSigWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "created": "2025-01-15T16:29:50Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z5Hk3nDNYGeuXfSqMTjiFsa2FLWbQn6KfXSioDc6PufxkngVgEQayTWZ5RpbBy9K2FU6tZacsWrQEqB2wgmsVKoUh" + }, + { + "type": "Ed25519Signature2020", + "created": "2025-01-15T16:34:28Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "zCXYPtKh6M5TEe4YTAk7FNPvLHfP7wrroxkfwECF7GK4GJjS9aRkMYw4Ns9E5PZ8GCZ53MV1bMzhsVuSWTLq885h" + } + ] +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1Expired.ts b/src/test-fixtures/verifiableCredentials/v1/v1Expired.ts new file mode 100644 index 0000000..0ada725 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1Expired.ts @@ -0,0 +1,54 @@ +export const v1Expired = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "issuanceDate": "2025-01-09T15:06:31Z", + "expirationDate": "2025-01-09T16:23:24Z", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": { + "type": [ + "Profile" + ], + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "name": "Digital Credentials Consortium Test Issuer", + "url": "https://dcconsortium.org", + "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" + }, + "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" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:56:21Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z44bwcZ2bQftiyUGKY6L7Gmg7iYfi9k6Va15osdm3KaKVnSW2DscpAMJVSs4UBf9riYReQ8VbZRf2qCY8W1rq2k3z" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1ExpiredWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v1/v1ExpiredWithValidStatus.ts new file mode 100644 index 0000000..073a8f6 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1ExpiredWithValidStatus.ts @@ -0,0 +1,60 @@ +export const v1ExpiredWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Taylor Tuna - Mock Bachelor of Science Degree in Biology", + "issuer": { + "url": "https://web.mit.edu/", + "type": "Profile", + "name": "Massachusetts Institute of Technology", + "image": { + "id": "https://github.com/digitalcredentials/test-files/assets/206059/01eca9f5-a508-40ac-9dd5-c12d11308894", + "type": "Image" + }, + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", + "https://www.w3.org/ns/credentials/status/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "Achievement" + ], + "name": "Mock Bachelor of Science in Biology", + "criteria": { + "type": "Criteria", + "narrative": "In Recognition of proficiency in the general and the special studies and exercises prescribed by said institution for such mock degree given this day under the seal of the Institute at Cambridge in the Commonwealth of Massachusetts" + }, + "description": "Massachusetts Institute of Technology Mock Bachelor of Science in Computer Science", + "fieldOfStudy": "Biology", + "achievementType": "BachelorDegree" + } + }, + "id": "urn:uuid:677fe54fcacf98774d482bcc", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#7", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "7" + }, + "issuanceDate": "2025-01-09T15:06:31Z", + "expirationDate": "2025-01-09T16:23:24Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T18:00:03Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z3mo89Ne4jeqGtDE86kAFBoQ2f4YzKMTvZTxi1knvYJQYxEe1Jhrk1PTXYdS4KXtdGF7ASESmmaoX4HTEdtJmrmYC" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1NoStatus.ts b/src/test-fixtures/verifiableCredentials/v1/v1NoStatus.ts new file mode 100644 index 0000000..28044b5 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1NoStatus.ts @@ -0,0 +1,49 @@ +export const v1NoStatus = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json', + 'https://w3id.org/security/suites/ed25519-2020/v1' + ], + id: 'urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c', + type: ['VerifiableCredential', 'OpenBadgeCredential'], + name: 'DCC Test Credential', + issuer: { + type: ['Profile'], + id: 'did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q', + 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' + }, + proof: { + type: 'Ed25519Signature2020', + created: '2023-10-05T11:17:41Z', + verificationMethod: + 'did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q', + proofPurpose: 'assertionMethod', + proofValue: + 'z5fk6gq9upyZvcFvJdRdeL5KmvHr69jxEkyDEd2HyQdyhk9VnDEonNSmrfLAcLEDT9j4gGdCG24WHhojVHPbRsNER' + } + } diff --git a/src/test-fixtures/verifiableCredentials/v1/v1Revoked.ts b/src/test-fixtures/verifiableCredentials/v1/v1Revoked.ts new file mode 100644 index 0000000..38cfd12 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1Revoked.ts @@ -0,0 +1,59 @@ +export const v1Revoked = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Sam Salmon - Mock Bachelor of Science Degree in Computer Science", + "issuer": { + "url": "https://web.mit.edu/", + "type": "Profile", + "name": "Massachusetts Institute of Technology", + "image": { + "id": "https://github.com/digitalcredentials/test-files/assets/206059/01eca9f5-a508-40ac-9dd5-c12d11308894", + "type": "Image" + }, + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", + "https://www.w3.org/ns/credentials/status/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Sam Salmon", + "achievement": { + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "Achievement" + ], + "name": "Mock Bachelor of Science in Computer Science", + "criteria": { + "type": "Criteria", + "narrative": "In Recognition of proficiency in the general and the special studies and exercises prescribed by said institution for such mock degree given this day under the seal of the Institute at Cambridge in the Commonwealth of Massachusetts" + }, + "description": "Massachusetts Institute of Technology Mock Bachelor of Science in Computer Science", + "fieldOfStudy": "Computer Science", + "achievementType": "BachelorDegree" + } + }, + "id": "urn:uuid:677fe54fcacf98774d482bcb", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#6", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "6" + }, + "issuanceDate": "2025-01-09T15:06:31Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:47:52Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "zbc8N9nkno2w6FimK67KnN2apUPeNgEWdyCgwEtEY8HvvhxJqCs7EetgxuWadAeGKrnD6wnetMAvxWTRniGFSzPd" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1RevokedAndExpired.ts b/src/test-fixtures/verifiableCredentials/v1/v1RevokedAndExpired.ts new file mode 100644 index 0000000..98742c7 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1RevokedAndExpired.ts @@ -0,0 +1,60 @@ +export const v1RevokedAndExpired = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Sam Salmon - Mock Bachelor of Science Degree in Computer Science", + "issuer": { + "url": "https://web.mit.edu/", + "type": "Profile", + "name": "Massachusetts Institute of Technology", + "image": { + "id": "https://github.com/digitalcredentials/test-files/assets/206059/01eca9f5-a508-40ac-9dd5-c12d11308894", + "type": "Image" + }, + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", + "https://www.w3.org/ns/credentials/status/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Sam Salmon", + "achievement": { + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "Achievement" + ], + "name": "Mock Bachelor of Science in Computer Science", + "criteria": { + "type": "Criteria", + "narrative": "In Recognition of proficiency in the general and the special studies and exercises prescribed by said institution for such mock degree given this day under the seal of the Institute at Cambridge in the Commonwealth of Massachusetts" + }, + "description": "Massachusetts Institute of Technology Mock Bachelor of Science in Computer Science", + "fieldOfStudy": "Computer Science", + "achievementType": "BachelorDegree" + } + }, + "id": "urn:uuid:677fe54fcacf98774d482bcb", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#6", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "6" + }, + "issuanceDate": "2025-01-09T15:06:31Z", + "expirationDate": "2025-01-09T16:23:24Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:53:13Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "zCFwdjxMQpWfVtdz85WC2i44qBjHZfJ4GzZ4weHtk48VN7E292P7X1NL7EXwtuEfax8wFPjh3cdhCVAedHNb2uZM" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1SimpleIssuerId.ts b/src/test-fixtures/verifiableCredentials/v1/v1SimpleIssuerId.ts new file mode 100644 index 0000000..f65fb7d --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1SimpleIssuerId.ts @@ -0,0 +1,45 @@ +export const v1SimpleIssuerId = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "urn:uuid:2fe53dc9-b2ec-4939-9b2c-0d00f6663b6c", + "issuanceDate": "2025-01-09T15:06:31Z", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "DCC Test Credential", + "issuer": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "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" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-29T16:27:25Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5FQSyBv3j1mxtVn5UjiXhBFo6tTx5mCHUk3bfwJQZHHmz2J18aeMRMzVieWoxNHEBUswV1ECFjSLnzDb44DcFB3C" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts new file mode 100644 index 0000000..bc9ef79 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v1/v1WithValidStatus.ts @@ -0,0 +1,59 @@ +export const v1WithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Taylor Tuna - Mock Bachelor of Science Degree in Biology", + "issuer": { + "url": "https://web.mit.edu/", + "type": "Profile", + "name": "Massachusetts Institute of Technology", + "image": { + "id": "https://github.com/digitalcredentials/test-files/assets/206059/01eca9f5-a508-40ac-9dd5-c12d11308894", + "type": "Image" + }, + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json", + "https://www.w3.org/ns/credentials/status/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", + "type": [ + "Achievement" + ], + "name": "Mock Bachelor of Science in Biology", + "criteria": { + "type": "Criteria", + "narrative": "In Recognition of proficiency in the general and the special studies and exercises prescribed by said institution for such mock degree given this day under the seal of the Institute at Cambridge in the Commonwealth of Massachusetts" + }, + "description": "Massachusetts Institute of Technology Mock Bachelor of Science in Computer Science", + "fieldOfStudy": "Biology", + "achievementType": "BachelorDegree" + } + }, + "id": "urn:uuid:677fe54fcacf98774d482bcc", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#7", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "7" + }, + "issuanceDate": "2025-01-09T15:15:26Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:45:28Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "zNq3fAUVhqHYJz1dJnw3kfMXjQK6xUTc4j2Zg8NjtVcCE5sXMiynVpCpPTK9jhUaVjZVNsc4XkDgcgsKMEUWTjU3" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/didWeb/v2WithBadDidWeb.ts b/src/test-fixtures/verifiableCredentials/v2/didWeb/v2WithBadDidWeb.ts new file mode 100644 index 0000000..26d4f6b --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/didWeb/v2WithBadDidWeb.ts @@ -0,0 +1,45 @@ +export const v2WithBadDidWeb = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:web:digitalcredentials.github.io:dcc-did-web-bad" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-21T17:20:42Z", + "verificationMethod": "did:web:digitalcredentials.github.io:dcc-did-web-bad#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z3exwX8VPoA2qME3WQdZ5RvCY9ahbUDDAXkm8Fb4zuCE5dnMWUVZrEN1q5hVmgroQq65WUwNiGHVgr2av5r7wRtb7" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.ts new file mode 100644 index 0000000..1463725 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/didWeb/v2didWebWithValidStatus.ts @@ -0,0 +1,52 @@ +export const v2didWebWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:web:digitalcredentials.github.io:dcc-did-web" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-20T21:40:14Z", + "verificationMethod": "did:web:digitalcredentials.github.io:dcc-did-web#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5ty6xS56YfvmD4wpCEBVnHv8a26Y6nmN1nYapS2mijK6WGCDXpjcFWKj15sYarJvbzvYkvp3Wwnpq54PT4VTdS5a" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2Expired.ts b/src/test-fixtures/verifiableCredentials/v2/v2Expired.ts new file mode 100644 index 0000000..e69709e --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2Expired.ts @@ -0,0 +1,46 @@ +export const v2Expired = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2011-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-07T22:14:25Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z2B4R2hwrcgR8ag39SZEgm2hAJSPnoWae7zdRr8RUTkcbMPNHu7tedk1x3D29J3CmiU5Wb1e7zew82nQAKYCQuRBo" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2ExpiredWithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v2/v2ExpiredWithValidStatus.ts new file mode 100644 index 0000000..3a43da2 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2ExpiredWithValidStatus.ts @@ -0,0 +1,53 @@ +export const v2ExpiredWithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2012-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T18:02:02Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z4GcediyH9xPL1nhauNuUpwoBcHb5u8y2MAMpAPG5DxbPXetT85fR4D6XJTpwSUEUmdwrhPWaDszCqTEdzw3Ke2HY" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2NoStatus.ts b/src/test-fixtures/verifiableCredentials/v2/v2NoStatus.ts new file mode 100644 index 0000000..54a9cf1 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2NoStatus.ts @@ -0,0 +1,45 @@ +export const v2NoStatus = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "type": [ + "Profile" + ], + "name": "Example Corp" + }, + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:58:33Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z62t6TYCERpTKuWCRhHc2fV7JoMhiFuEcCXGkX9iit8atQPhviN5cZeZfXRnvJWa3Bm6DjagKyrauaSJfp9C9i7q3" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2Revoked.ts b/src/test-fixtures/verifiableCredentials/v2/v2Revoked.ts new file mode 100644 index 0000000..655126f --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2Revoked.ts @@ -0,0 +1,52 @@ +export const v2Revoked = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Sam Salmon", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Bachelors - v2 - revoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d06", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#8", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "8" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:40:46Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5nD6P3KrB9mTs6wSJuP8iJ3T2G9mWZuZnJ6wkYZoBWnZ2KD3ZzFTzvvpPD4HtoKhP8Fs9uewxMwc3yfdfzCV85sr" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2RevokedAndExpired.ts b/src/test-fixtures/verifiableCredentials/v2/v2RevokedAndExpired.ts new file mode 100644 index 0000000..6a623ef --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2RevokedAndExpired.ts @@ -0,0 +1,53 @@ +export const v2RevokedAndExpired = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2011-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Sam Salmon", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Bachelors - v2 - revoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d06", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#8", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "8" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:49:31Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z4a51wePqfcFXExrPeQNUZ1d7xN4iH8N4gwuv4wcfdTM6EYjRgu5obWyA1W86pyhF7L3sZAXQ3QRSAHTCvUTwU1qL" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2SimpleIssuerId.ts b/src/test-fixtures/verifiableCredentials/v2/v2SimpleIssuerId.ts new file mode 100644 index 0000000..52cdfe8 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2SimpleIssuerId.ts @@ -0,0 +1,39 @@ +export const v2SimpleIssuerId = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.com/credentials/3527", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "validFrom": "2010-01-01T00:00:00Z", + "name": "Teamwork Badge", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-29T16:29:21Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z2QqZBSqcJjydA9qaMrc9ddk5Tb65wmN1qZCXgmXrsghfzcXRgsVZ9ZyLQZLbgpox65GBRxh78J7nDznVksbU5EH1" + } +} \ No newline at end of file diff --git a/src/test-fixtures/verifiableCredentials/v2/v2WithValidStatus.ts b/src/test-fixtures/verifiableCredentials/v2/v2WithValidStatus.ts new file mode 100644 index 0000000..b53fdb1 --- /dev/null +++ b/src/test-fixtures/verifiableCredentials/v2/v2WithValidStatus.ts @@ -0,0 +1,52 @@ +export const v2WithValidStatus = { + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "Teamwork Badge", + "issuer": { + "type": [ + "Profile" + ], + "name": "Example Corp", + "id": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q" + }, + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "validFrom": "2010-01-01T00:00:00Z", + "credentialSubject": { + "type": [ + "AchievementSubject" + ], + "name": "Taylor Tuna", + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "name": "Masters - v2 - unrevoked", + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment." + } + }, + "id": "urn:uuid:677fe8a6cacf98774d482d07", + "credentialStatus": { + "id": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj#9", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListCredential": "https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5WK8CbZ1GjycuPombrj", + "statusListIndex": "9" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2025-01-09T17:39:28Z", + "verificationMethod": "did:key:z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q#z6MknNQD1WHLGGraFi6zcbGevuAgkVfdyCdtZnQTGWVVvR5Q", + "proofPurpose": "assertionMethod", + "proofValue": "z5ZzyLDUsPtvXM4o7atD5n14vg8BFh4RRUppDvbWeg3e1cGWqRvchu7npkjQK4mLsxTt81Bqqqv4necWcse734Y1S" + } +} \ No newline at end of file diff --git a/src/types/credential.ts b/src/types/credential.ts new file mode 100644 index 0000000..5dcc835 --- /dev/null +++ b/src/types/credential.ts @@ -0,0 +1,154 @@ + + + +export type IssuerURI = string; + +export interface ImageObject { + readonly id: string; + readonly type: string; +} + +export interface IssuerObject { + readonly id: IssuerURI; + readonly type?: string; + readonly name?: string; + readonly url?: string; + readonly image?: string | ImageObject; +} +export type Issuer = IssuerURI | IssuerObject; + +export interface CreditValue { + value?: string; +} + +export interface CompletionDocument { + readonly type?: string; + readonly identifier?: string; + readonly name?: string; + readonly description?: string; + readonly numberOfCredits?: CreditValue; + readonly startDate?: string; + readonly endDate?: string; +} + +export interface EducationalOperationalCredentialExtensions { + readonly type?: string[]; + readonly awardedOnCompletionOf?: CompletionDocument; + readonly criteria?: { + type: string; + narrative: string; + }; + readonly image?: ImageObject; +} + +// https://schema.org/EducationalOccupationalCredential (this doesn't really conform) +export type EducationalOperationalCredential = EducationalOperationalCredentialExtensions & { + readonly id?: string; + readonly name?: string; + readonly description?: string; + readonly competencyRequired?: string; + readonly credentialCategory?: string; + readonly achievementType?: string; +} + +export interface DegreeCompletion { + readonly type: string; + readonly name: string; +} + +export interface StudentId { + readonly id: string; + readonly image: string; +} + +interface SubjectExtensions { + readonly type?: string; + readonly name?: string; + readonly hasCredential?: EducationalOperationalCredential; // https://schema.org/hasCredential + readonly degree?: DegreeCompletion; + readonly studentId?: StudentId; + // Open Badges v3 + readonly achievement?: EducationalOperationalCredential | EducationalOperationalCredential[]; + readonly identifier?: OBV3IdentifierObject | OBV3IdentifierObject[]; +} + +export interface OBV3IdentifierObject { + readonly identityType?: string; + readonly identityHash?: string; +} + +export type Subject = SubjectExtensions & { + readonly id?: string; +} + +export interface Proof { + type: string; + created: string; + verificationMethod: string; + proofPurpose: string; + proofValue: string; + cryptosuite?: string; + challenge?: string; + jws?: string; +} + +export interface RenderMethod { + id?: string; + type: string; + name?: string; + css3MediaQuery?: string; +} + +// https://www.w3.org/TR/vc-data-model/ +export interface CredentialV1 { + readonly '@context': string[]; // https://www.w3.org/TR/vc-data-model/#contexts + readonly id?: string; // https://www.w3.org/TR/vc-data-model/#identifiers + readonly type: string[]; // https://www.w3.org/TR/vc-data-model/#types + readonly issuer: Issuer; // https://www.w3.org/TR/vc-data-model/#issuer + readonly issuanceDate: string; // https://www.w3.org/TR/vc-data-model/#issuance-date + readonly expirationDate?: string; // https://www.w3.org/TR/vc-data-model/#expiration + readonly credentialSubject: Subject; // https://www.w3.org/TR/vc-data-model/#credential-subject + readonly credentialStatus?: CredentialStatus | CredentialStatus[]; // https://www.w3.org/TR/vc-data-model/#status + readonly proof?: Proof; // https://www.w3.org/TR/vc-data-model/#proofs-signatures + readonly name?: string; + readonly renderMethod?: RenderMethod[]; +} + +// https://www.w3.org/TR/vc-data-model-2.0/ +// (At this time, this should be in sync with https://w3c.github.io/vc-data-model/) +export interface CredentialV2 { + readonly '@context': string[]; // https://www.w3.org/TR/vc-data-model-2.0/#contexts + readonly id?: string; // https://www.w3.org/TR/vc-data-model-2.0/#identifiers + readonly type: string[]; // https://www.w3.org/TR/vc-data-model-2.0/#types + readonly issuer: Issuer; // https://www.w3.org/TR/vc-data-model-2.0/#issuer + readonly validFrom?: string; // https://www.w3.org/TR/vc-data-model-2.0/#validity-period + readonly validUntil?: string; // https://www.w3.org/TR/vc-data-model-2.0/#validity-period + readonly credentialSubject: Subject; // https://www.w3.org/TR/vc-data-model-2.0/#credential-subject + readonly credentialStatus?: CredentialStatus | CredentialStatus[]; // https://www.w3.org/TR/vc-data-model-2.0/#status + readonly proof?: Proof; // https://w3c.github.io/vc-data-model/#proofs-signatures + readonly name?: string; + readonly renderMethod?: RenderMethod[]; // https://www.w3.org/TR/vc-data-model-2.0/#reserved-extension-points +} + +export type Credential = CredentialV1 | CredentialV2; + +// https://www.w3.org/TR/vc-bitstring-status-list +export interface CredentialStatus { + readonly id: string; + readonly type: string | string[]; + readonly statusPurpose: string; + readonly statusListIndex: string | number; + readonly statusListCredential: string; +} + +export enum CredentialError { + IsNotVerified = 'Credential is not verified.', + CouldNotBeVerified = 'Credential could not be checked for verification and may be malformed.', + DidNotInRegistry = 'Could not find issuer in registry with given DID.', +} + +export interface CredentialImportReport { + success: string[]; + duplicate: string[]; + failed: string[]; +} diff --git a/src/types/presentation.ts b/src/types/presentation.ts new file mode 100644 index 0000000..e252ad8 --- /dev/null +++ b/src/types/presentation.ts @@ -0,0 +1,14 @@ +import type { Credential, Proof, Issuer } from './credential'; + +export interface VerifiablePresentation { + readonly '@context': string[]; + readonly issuer: Issuer; + readonly type: string; + readonly verifiableCredential: Credential | Credential[]; + readonly proof: Proof; +} + +export enum PresentationError { + IsNotVerified = 'Presentation is not verified.', + CouldNotBeVerified = 'Presentation encoded could not be checked for verification and may be malformed.', +} diff --git a/src/types/result.ts b/src/types/result.ts new file mode 100644 index 0000000..335a05d --- /dev/null +++ b/src/types/result.ts @@ -0,0 +1,42 @@ + +export interface VerificationError { + "message": string, + "name"?: string, + "stackTrace"?: any + } + + export interface VerificationStep { + "id": string, + "valid"?: boolean, + "foundInRegistries"?: string[], + "registriesNotLoaded"?: RegistriesNotLoaded[], + "error"?: VerificationError + } + + export interface VerificationResponse { + "credential"?: object, + "errors"?: VerificationError[], + "log"?: VerificationStep[] + } + + + const signatureOptions = ['valid', 'invalid', 'unsigned'] as const; + export type PresentationSignatureResult = typeof signatureOptions[number]; // i.e., 'valid', 'invalid', 'unsigned' + + export interface PresentationResult { + "signature":PresentationSignatureResult, + "error"?: any + } + + export interface PresentationVerificationResponse { + "credentialResults"?: VerificationResponse[], + "presentationResult"?: PresentationResult, + "errors"?: VerificationError[] + } + + export interface RegistryListResult { + foundInRegistries: string[] + registriesNotLoaded: RegistriesNotLoaded[] + } + + export interface RegistriesNotLoaded {name: string, url: string} \ No newline at end of file diff --git a/test/Example.spec.ts b/test/Example.spec.ts deleted file mode 100644 index 3bf5e63..0000000 --- a/test/Example.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expect } from 'chai' -import { Example } from '../src' - -describe('Example', () => { - it('calls function', async () => { - const ex = new Example() - expect(ex.hello()).to.equal('world') - }) -}) diff --git a/test/Verify.general.spec.ts b/test/Verify.general.spec.ts new file mode 100644 index 0000000..639d82f --- /dev/null +++ b/test/Verify.general.spec.ts @@ -0,0 +1,72 @@ +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' +import { verifyCredential } from '../src/Verify.js' +import { + getCredentialWithoutContext, + getCredentialWithoutVCContext, +} from '../src/test-fixtures/vc.js' +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; +import { + getExpectedFatalResult + } from '../src/test-fixtures/expectedResults.js'; +import { INVALID_JSONLD, NO_VC_CONTEXT } from '../src/constants/errors.js'; + +chai.use(deepEqualInAnyOrder); +const {expect} = chai; + +const DISABLE_CONSOLE_WHEN_NO_ERRORS = true + +describe('Verify', () => { + + const originalLogFunction = console.log; + let output:string; + + beforeEach(function(done) { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + } + done() + }); + + afterEach(function() { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } + } + }); + + describe('.verifyCredential', () => { + + describe('returns fatal errors', () => { + + it('when not jsonld', async () => { + const credential : any = getCredentialWithoutContext() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The credential does not appear to be a valid jsonld document - there is no context.', + errorName: INVALID_JSONLD + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + it('when no vc context', async () => { + const credential : any = getCredentialWithoutVCContext() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential doesn't have a verifiable credential context.", + errorName: NO_VC_CONTEXT + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + }) +}) +}) \ No newline at end of file diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts new file mode 100644 index 0000000..59207dd --- /dev/null +++ b/test/Verify.presentation.spec.ts @@ -0,0 +1,251 @@ +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' +import { verifyPresentation } from '../src/Verify.js' +import { + getVCv1SimpleIssuerId, + getVCv2SimpleIssuerId, + getVCv2Expired, + getVCv2Revoked, + getVCv2ValidStatus, + getVCv2Tampered, + getVCv2NoProof, + getCredentialWithoutContext, + getCredentialWithoutVCContext, + getVCv2NonURIId, + getVCv2ExpiredAndTampered, + getVCv2ExpiredWithValidStatus, + getVCv2EddsaWithValidStatus, + getVCv2DoubleSigWithBadStatusUrl, + getVCv2DidWebWithValidStatus, + getVCv2WithBadDidWebUrl + +} from '../src/test-fixtures/vc.js' + +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; +import { + getExpectedVerifiedResult, + getExpectedUnverifiedResult, + getExpectedFatalResult, + getExpectedVerifiedPresentationResult + } from '../src/test-fixtures/expectedResults.js'; + + import { + getVCv1, + getVCv1Tampered, + getVCv1Expired, + getVCv1Revoked, + getVCv1ValidStatus, + getVCv1NoProof, + getVCv1NonURIId, + getVCv1ExpiredAndTampered, + getVCv1ExpiredWithValidStatus +} from '../src/test-fixtures/vc.js' + +import { getSignedVP, getUnSignedVP } from './vpUtils.js'; +import { VerifiablePresentation } from '../src/types/presentation.js'; +import { INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF, PRESENTATION_ERROR } from '../src/constants/errors.js'; +import { SIGNATURE_INVALID, SIGNATURE_UNSIGNED } from '../src/constants/verificationSteps.js'; + + + const noProofVC : any = getVCv1NoProof() + const expectedNoProofResult = getExpectedFatalResult({ + credential: noProofVC, + errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', + errorName: NO_PROOF + }) + + + const badIdVC : any = getVCv2NonURIId() + const expectedBadIdResult = getExpectedFatalResult({ + credential: badIdVC, + errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", + errorName: INVALID_CREDENTIAL_ID + }) + + + const didWebVC : any = getVCv2DidWebWithValidStatus() + const expectedDidWebResult = getExpectedVerifiedResult({credential:didWebVC, withStatus: true}) + + const v2WithStatus : any = getVCv2ValidStatus() + const expectedV2WithStatusResult = getExpectedVerifiedResult({credential:v2WithStatus, withStatus: true}) + + const v1NoStatus : any = getVCv1() + const expectedV1Result = getExpectedVerifiedResult({credential:v1NoStatus, withStatus: false}) + + + const v2Eddsa : any = getVCv2EddsaWithValidStatus() + const expectedv2EddsaResult = getExpectedVerifiedResult({credential: v2Eddsa, withStatus: true}) + + const v1SimpleIssuerId : any = getVCv1SimpleIssuerId() + const expectedV1SimpleIssuerResult = getExpectedVerifiedResult({credential:v1SimpleIssuerId, withStatus: false}) + + const v2SimpleIssuerId : any = getVCv2SimpleIssuerId() + const expectedV2SimpleIssuerResult = getExpectedVerifiedResult({credential:v2SimpleIssuerId, withStatus: false}) + +chai.use(deepEqualInAnyOrder); +const {expect} = chai; + +const DISABLE_CONSOLE_WHEN_NO_ERRORS = true + + +describe('Verify.verifyPresentation', () => { + + const holder = 'did:ex:12345'; + + const originalLogFunction = console.log; + let output:string; + + beforeEach(function(done) { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + } + done() + }); + + afterEach(function() { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } + } + }); + + describe('it returns as verified', () => { + + it('with v1 and v2 vcs with simple issuer ids', async () => { + const verifiableCredential= [v1SimpleIssuerId, v2SimpleIssuerId] + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedV1SimpleIssuerResult,expectedV2SimpleIssuerResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when signed presentation has one vc in an array', async () => { + const verifiableCredential= [v2WithStatus] + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedV2WithStatusResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when unsigned presentation has one vc not in an array', async () => { + const verifiableCredential= v2WithStatus + const presentation = await getUnSignedVP({verifiableCredential}) as any + presentation.verifiableCredential = presentation.verifiableCredential[0] + const credentialResults = [expectedV2WithStatusResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults, unsigned:true}) + const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation: true}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when signed presentation has mix of VCs', async () => { + const verifiableCredential = [v2WithStatus, v2Eddsa, didWebVC] + const presentation = await getSignedVP({verifiableCredential, holder: 'did:ex:12345'}) as VerifiablePresentation + const credentialResults = [expectedV2WithStatusResult, expectedv2EddsaResult, expectedDidWebResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when wrong challenge and presentation purpose', async () => { + const verifiableCredential= [v2WithStatus] + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedV2WithStatusResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries, challenge: 'blahblahblue'}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + }) + + describe('it returns as unverified', () => { + + + + it('when vc in signed presentation has been tampered with', async () => { + const v1 : any = getVCv1() + const verifiableCredential= [v1] + const presentation = await getSignedVP({holder, verifiableCredential}) as any + presentation.verifiableCredential[0].name = 'Tampered Name' + const result = await verifyPresentation({presentation, knownDIDRegistries}) as any + expect(result.presentationResult.signature).to.equal(SIGNATURE_INVALID) + expect(result.credentialResults[0].errors[0].name).to.equal(INVALID_SIGNATURE) + }) + + it('when signed presentation has been tampered with', async () => { + const verifiableCredential= [v1NoStatus] + const presentation = await getSignedVP({holder, verifiableCredential}) as any + presentation.holder = 'did:ex:tampered' + const result = await verifyPresentation({presentation, knownDIDRegistries}) as any + const expectedCredentialResults = [expectedV1Result] + expect(result.credentialResults).to.deep.equalInAnyOrder(expectedCredentialResults) + expect(result.presentationResult.signature).to.equal(SIGNATURE_INVALID) + }) + + it('when unsigned presentation has bad vc', async () => { + /// NOTE that this is an unsigned vp because the vc libs signing + // method doesn't allow signing a VP with a 'bad' VC, so + // we can't easily get a test vp + const verifiableCredential= [badIdVC] + const presentation = await getUnSignedVP({verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedBadIdResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults, unsigned: true}) + const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation: true}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + + }) + + it('when signed presentation has no proof vc', async () => { + const verifiableCredential= [noProofVC] + const presentation = await getSignedVP({holder, verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedNoProofResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + const result = await verifyPresentation({presentation, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when unsigned presentation', async () => { + const verifiableCredential= [noProofVC] + const presentation = getUnSignedVP({verifiableCredential}) as VerifiablePresentation + const credentialResults = [expectedNoProofResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({credentialResults}) + if (expectedPresentationResult?.presentationResult) { + expectedPresentationResult.presentationResult.signature = SIGNATURE_UNSIGNED + } + const result = await verifyPresentation({presentation, knownDIDRegistries, unsignedPresentation:true}) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when unsigned presentation not properly specified', async () => { + const verifiableCredential= [noProofVC] + const presentation = await getUnSignedVP({verifiableCredential}) as VerifiablePresentation + const result = await verifyPresentation({presentation, knownDIDRegistries}) + expect(result?.presentationResult?.signature).to.equal(SIGNATURE_INVALID) + }) + + it('when bad presentation', async () => { + const verifiableCredential= [noProofVC] + const presentation = await getUnSignedVP({verifiableCredential}) as any + delete presentation['@context'] + const result = await verifyPresentation({presentation, knownDIDRegistries}) + if (result?.errors) { + expect(result.errors[0].name).to.equal(PRESENTATION_ERROR) + } else { + expect(false).to.equal(true) + } + + }) + + }) + +}) + + + + diff --git a/test/Verify.v1.spec.ts b/test/Verify.v1.spec.ts new file mode 100644 index 0000000..fa1b8f4 --- /dev/null +++ b/test/Verify.v1.spec.ts @@ -0,0 +1,215 @@ +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' +import { strict as assert } from 'assert'; +import { verifyCredential } from '../src/Verify.js' +import { + getVCv1Tampered, + getVCv1Expired, + getVCv1Revoked, + getVCv1ValidStatus, + getVCv1NoProof, + getVCv1NonURIId, + getVCv1ExpiredAndTampered, + getVCv1ExpiredWithValidStatus + +} from '../src/test-fixtures/vc.js' +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; +import { + getExpectedVerifiedResult, + getExpectedUnverifiedResult, + getExpectedFatalResult + } from '../src/test-fixtures/expectedResults.js'; +import { INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF } from '../src/constants/errors.js'; +import { EXPIRATION_STEP_ID, REGISTERED_ISSUER_STEP_ID } from '../src/constants/verificationSteps.js'; + +chai.use(deepEqualInAnyOrder); +const {expect} = chai; + +const DISABLE_CONSOLE_WHEN_NO_ERRORS = true +/* +tests to add: + +- simulatenouly expired and revoked for v1 and v2 +- expired but valid status for v1 and v2 +- simultaneosly expired and tampered for v1 and v2 +- returns registry entries that are retunred by nock call +- returns no registry entry for nock with no result +- returns unverified for issuer DID that doesn't resolve +- returns verified when no status property + +*/ + + + +describe('Verify', () => { + + const originalLogFunction = console.log; + let output:string; + + beforeEach(function(done) { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + } + done() + }); + + afterEach(function() { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } + } + }); + + describe('.verifyCredential', () => { + + describe('with VC version 1', () => { + + describe('returns fatal error', () => { + it('when tampered with', async () => { + const credential : any = getVCv1Tampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: INVALID_SIGNATURE + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('when expired and tampered with', async () => { + const credential : any = getVCv1ExpiredAndTampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: INVALID_SIGNATURE + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('when no proof', async () => { + const credential : any = getVCv1NoProof() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', + errorName: NO_PROOF + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + it('when credential id is not a uri', async () => { + const credential : any = getVCv1NonURIId() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", + errorName: INVALID_CREDENTIAL_ID + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + }) + + describe('returns as verified', () => { + it('when status is valid', async () => { + const credential : any = getVCv1ValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + }) + + describe('returns unverified', () => { + it('when expired', async () => { + const credential : any = getVCv1Expired() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: EXPIRATION_STEP_ID, withStatus:false}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + it('when revoked', async () => { + const credential : any = getVCv1Revoked() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + assert.ok(result.log); + }) + + it('when expired with valid status', async () => { + const credential : any = getVCv1ExpiredWithValidStatus() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: EXPIRATION_STEP_ID, withStatus:true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('when no matching registry', async () => { + const credential : any = getVCv1ValidStatus() + const noMatchingRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) + // set the one matching registry to a url that won't load + noMatchingRegistryList[1].url = 'https://onldynoyrt.com/registry.json' + const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) + const expectedResultRegistryLogEntry = expectedResult.log.find((entry:any)=>entry.id===REGISTERED_ISSUER_STEP_ID) + expectedResultRegistryLogEntry.registriesNotLoaded = [ + { + "name": "DCC Sandbox Registry", + "url": "https://onldynoyrt.com/registry.json" + } + ] + expectedResultRegistryLogEntry.valid = false; + expectedResultRegistryLogEntry.foundInRegistries = [] + + const result = await verifyCredential({credential, reloadIssuerRegistry: true, knownDIDRegistries: noMatchingRegistryList}) + + //console.log(JSON.stringify(result, null, 2)) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + }) + + describe('returns accurate registry list', () => { + + it('when one registry url does not exist', async () => { + const credential : any = getVCv1ValidStatus() + const badRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) + badRegistryList[0].url = 'https://onldynoyrt.com/registry.json' + const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) + expectedResult.log.find((entry:any)=>entry.id===REGISTERED_ISSUER_STEP_ID).registriesNotLoaded = [ + { + "name": "DCC Pilot Registry", + "url": "https://onldynoyrt.com/registry.json" + } + ] + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries: badRegistryList}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('when two registry urls do not exist', async () => { + const credential : any = getVCv1ValidStatus() + const badRegistryList = JSON.parse(JSON.stringify(knownDIDRegistries)) + badRegistryList[0].url = 'https://onldynoyrt.com/registry.json' + badRegistryList[2].url = 'https://onldynoyrrrt.com/registry.json' + const expectedResult : any = getExpectedVerifiedResult({credential, withStatus: true}) + expectedResult.log.find((entry:any)=>entry.id===REGISTERED_ISSUER_STEP_ID).registriesNotLoaded = [ + { + "name": "DCC Community Registry", + "url": "https://onldynoyrrrt.com/registry.json" + }, + { + "name": "DCC Pilot Registry", + "url": "https://onldynoyrt.com/registry.json" + } + ] + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries: badRegistryList}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + it('when all registries exist', async () => { + const credential : any = getVCv1ValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + }) + }) +}) +}) + diff --git a/test/Verify.v2.spec.ts b/test/Verify.v2.spec.ts new file mode 100644 index 0000000..ec6c16c --- /dev/null +++ b/test/Verify.v2.spec.ts @@ -0,0 +1,207 @@ +import chai from 'chai' +import deepEqualInAnyOrder from 'deep-equal-in-any-order' +import { strict as assert } from 'assert'; +import { verifyCredential } from '../src/Verify.js' +import { + getVCv2Expired, + getVCv2Revoked, + getVCv2ValidStatus, + getVCv2Tampered, + getVCv2NoProof, + getCredentialWithoutContext, + getCredentialWithoutVCContext, + getVCv2NonURIId, + getVCv2ExpiredAndTampered, + getVCv2ExpiredWithValidStatus, + getVCv2EddsaWithValidStatus, + getVCv2DoubleSigWithBadStatusUrl, + getVCv2DidWebWithValidStatus, + getVCv2WithBadDidWebUrl + +} from '../src/test-fixtures/vc.js' +import { knownDIDRegistries } from '../src/test-fixtures/knownDidRegistries.js'; +import { + getExpectedVerifiedResult, + getExpectedUnverifiedResult, + getExpectedFatalResult + } from '../src/test-fixtures/expectedResults.js'; +import { EXPIRATION_STEP_ID, REVOCATION_STATUS_STEP_ID } from '../src/constants/verificationSteps.js'; +import { DID_WEB_UNRESOLVED, INVALID_CREDENTIAL_ID, INVALID_SIGNATURE, NO_PROOF, STATUS_LIST_NOT_FOUND } from '../src/constants/errors.js'; + +chai.use(deepEqualInAnyOrder); +const {expect} = chai; + +const DISABLE_CONSOLE_WHEN_NO_ERRORS = true + +describe('Verify', () => { + + const originalLogFunction = console.log; + let output:string; + + beforeEach(function(done) { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + output = ''; + console.log = (msg) => { + output += msg + '\n'; + }; + } + done() + }); + + afterEach(function() { + if (DISABLE_CONSOLE_WHEN_NO_ERRORS) { + console.log = originalLogFunction; // undo dummy log function + if (this?.currentTest?.state === 'failed') { + console.log(output); + } + } + }); + + describe('.verifyCredential', () => { + + describe('with VC version 2', () => { + + describe('and ed25519 and eddsa signature', () => { + describe('returns log error', () => { + it('when statuslist url is unreachable', async () => { + const credential : any = getVCv2DoubleSigWithBadStatusUrl() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: false}) + expectedResult.log?.push( + { + "id": REVOCATION_STATUS_STEP_ID, + "error": { + "name": STATUS_LIST_NOT_FOUND, + "message": "NotFoundError loading \"https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj\": Request failed with status code 404 Not Found: GET https://raw.githubusercontent.com/digitalcredentials/verifier-core/refs/heads/main/src/test-fixtures/status/e5VK8CbZ1GjycuPombrj" + } + }) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + }) + }) + describe('with eddsa signature', () => { + describe('it returns as verified', () => { + it('when status is valid', async () => { + const credential : any = getVCv2EddsaWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + // console.log(JSON.stringify(result,null,2)) + + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + }) + + + describe('returns fatal error', () => { + + it('when tampered with', async () => { + const credential : any = getVCv2Tampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: INVALID_SIGNATURE + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + it('when expired and tampered with', async () => { + const credential : any = getVCv2ExpiredAndTampered() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'The signature is not valid.', + errorName: INVALID_SIGNATURE + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('when no proof', async () => { + const credential : any = getVCv2NoProof() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: 'This is not a Verifiable Credential - it does not have a digital signature.', + errorName: NO_PROOF + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + it('when credential id is not a uri', async () => { + const credential : any = getVCv2NonURIId() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + + const expectedResult = getExpectedFatalResult({ + credential, + errorMessage: "The credential's id uses an invalid format. It may have been issued as part of an early pilot. Please contact the issuer to get a replacement.", + errorName: INVALID_CREDENTIAL_ID + }) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + it('when did:web url is unreachable', async () => { + const credential : any = getVCv2WithBadDidWebUrl() + const errorName = DID_WEB_UNRESOLVED + const errorMessage = "The signature could not be checked because the public signing key could not be retrieved from https://digitalcredentials.github.io/dcc-did-web-bad/did.json" + const expectedResult = getExpectedFatalResult({credential, errorName, errorMessage}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + + }) + + describe('returns as verified', () => { + it('when status is valid', async () => { + const credential : any = getVCv2ValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + describe('with did:web issuer', () => { + + it('when status is valid', async () => { + const credential : any = getVCv2DidWebWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + it('with different issuer for vc and statusList ', async () => { + const credential : any = getVCv2DidWebWithValidStatus() + const expectedResult = getExpectedVerifiedResult({credential, withStatus: true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + }) + + }) + + describe('returns as unverified', () => { + it('when expired', async () => { + const credential : any = getVCv2Expired() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: EXPIRATION_STEP_ID, withStatus:false}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) + }) + it('when revoked', async () => { + const credential : any = getVCv2Revoked() + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + assert.ok(result.log); + }) + it('when expired with valid status', async () => { + // NOTE: TODO - this will continue to fail until we fix https://github.com/digitalcredentials/vc/issues/28 + const credential : any = getVCv2ExpiredWithValidStatus() + const expectedResult = getExpectedUnverifiedResult({credential, unVerifiedStep: EXPIRATION_STEP_ID, withStatus:true}) + const result = await verifyCredential({credential, reloadIssuerRegistry: false, knownDIDRegistries}) + expect(result).to.deep.equalInAnyOrder(expectedResult) // eslint-disable-line no-use-before-define + }) + + }) + }) +}) +}) +}) \ No newline at end of file diff --git a/test/vpUtils.ts b/test/vpUtils.ts new file mode 100644 index 0000000..060f14a --- /dev/null +++ b/test/vpUtils.ts @@ -0,0 +1,48 @@ +import {verify,signPresentation,createPresentation} from '@digitalcredentials/vc'; + +import {Ed25519Signature2020} from '@digitalcredentials/ed25519-signature-2020'; +import { securityLoader } from '@digitalcredentials/security-document-loader'; +import {Ed25519VerificationKey2020} from '@digitalcredentials/ed25519-verification-key-2020'; + +const documentLoader = securityLoader().build() + +import pkg from '@digitalcredentials/jsonld-signatures'; +import { VerifiablePresentation } from '../src/types/presentation'; +const { purposes } = pkg; +const presentationPurpose = new purposes.AssertionProofPurpose(); + +const key = await Ed25519VerificationKey2020.generate( + { + seed: new Uint8Array ([ + 217, 87, 166, 30, 75, 106, 132, 55, + 32, 120, 171, 23, 116, 73, 254, 74, + 230, 16, 127, 91, 2, 252, 224, 96, + 184, 172, 245, 157, 58, 217, 91, 240 + ]), + controller: "did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP" + } +) + + +const signingSuite = new Ed25519Signature2020({key}); + +export const getSignedVP = async ({holder, verifiableCredential}:{holder:string,verifiableCredential?:any}):Promise => { + const presentation = createPresentation({holder, verifiableCredential}); + const challenge = 'canbeanything33' + return await signPresentation({ + presentation, suite:signingSuite, documentLoader, challenge, purpose: presentationPurpose + }); +} + +export const getUnSignedVP = ({verifiableCredential}:{verifiableCredential?:any}):VerifiablePresentation => { + return createPresentation({verifiableCredential}); +} + + +const verificationSuite = new Ed25519Signature2020(); + +export const verifyDIDAuth = async ({presentation, challenge}:{presentation:any,challenge:string}):Promise => { + const result = await verify({presentation, challenge, suite: verificationSuite, documentLoader}); + return result +} + diff --git a/tsconfig.json b/tsconfig.json index e947de9..3000add 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,9 @@ "strict": true, "target": "es2022", "lib": ["es2022", "dom"], - "module": "commonjs", + "module": "es6", "moduleResolution": "node", - "outDir": "dist/esm", + "outDir": "dist", "noImplicitAny": true, "removeComments": false, "preserveConstEnums": true, @@ -19,8 +19,8 @@ }, "include": [ "src/**/*", - ".eslintrc.js", - "karma.conf.js" - ], + ".eslintrc.cjs", + "karma.conf.cjs" +, "src/test-fixtures/knownDidRegistries.ts" ], "exclude": ["node_modules", "dist", "test"] } diff --git a/tsconfig.spec.json b/tsconfig.spec.json index e947de9..e1b17c6 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -3,9 +3,9 @@ "strict": true, "target": "es2022", "lib": ["es2022", "dom"], - "module": "commonjs", + "module": "es2022", "moduleResolution": "node", - "outDir": "dist/esm", + "outDir": "dist", "noImplicitAny": true, "removeComments": false, "preserveConstEnums": true, @@ -19,8 +19,9 @@ }, "include": [ "src/**/*", + "test/**/*", ".eslintrc.js", "karma.conf.js" ], - "exclude": ["node_modules", "dist", "test"] + "exclude": ["node_modules", "dist"] }