Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 100 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ Variations and errors are covered next...

There are three general flavours of result that might be returned:

- all checks were conclusive
- verification was partially successful
- verification was fatal

1. <b>all checks were conclusive</b>

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.
Expand Down Expand Up @@ -222,7 +226,7 @@ A conclusive verification might look like this example where all steps returned
}
```

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.
An invalid signature is considered fatal, rather than conclusive (even though in a sense it conclusively rejects the entire credential) because an invalid signature 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 those steps, and can't even check them because they could be fraudulent.

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:

Expand Down Expand Up @@ -268,23 +272,21 @@ And here is a slightly different verification result where we have still made co

2. <b> partially successful verification</b>

A verification might partly succeed if it can verify:

* the signature
* the expiry date
A verification might partly succeed if it can conclusively determine some of the steps - most
importantly that the credential hasn't been tampered with - but can't conclusively verify (as true or false) some other steps.

But can't retrieve (from the network) any one of:
A good example is if there are network problems and the verifier can't retrieve things an issuer registry, and so can't say whether the did used to sign the VC is listed in the registry. It might be, but it might not be.

* the revocation status
* an issuer registry from our list of trusted issuers
Another example is the revocation status - if we can't retrieve the status list from the network then we again
can't see one way or the other if the credential has been revoked. It might have been, it might not have been.

which are needed to verify the revocation status and issuer identity.
The revocation status is an especially interesting example because the status list is itself a Verifiable Credential, which could have expired, been revoked, or been tampered with. And if so, then we again can't say anything about the status of the VC we are trying to to verify because the status list is not valid.

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:
A partially successful verification might look like this example, where we couldn't retrieve one of the registries:

```
{
Expand All @@ -298,13 +300,6 @@ A partially successful verification might look like this example, where we could
"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,
Expand Down Expand Up @@ -341,6 +336,94 @@ A partially successful verification might look like this example, where we could
}
```

Or for a status list that couldn't be retrieved:

```
{
"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": true,
"matchingIssuers": [
{
"issuer": {
"federation_entity": {
"organization_name": "DCC did:web test",
"homepage_uri": "https://digitalcredentials.mit.edu",
"location": "Cambridge, MA, USA"
}
},
"registry": {
"name": "DCC Sandbox Registry",
"type": "dcc-legacy",
"url": "https://digitalcredentials.github.io/sandbox-registry/registry.json"
}
}
]
}
]
}
```

The status list errors that we return include:
```
"error": {
"name": "status_list_not_found",
"message": "Could not retrieve the revocation status list."
}
```

```
"error": {
"name": "status_list_expired",
"message": "The status list verifiable credential has expired."
}
```

```
"error": {
"name": "status_list_signature_error",
"message": "The signature on the status list is invalid."
}
```

```
"error": {
"name": "status_list_type_error",
"message": "Status list credential type must include \"BitstringStatusListCredential\"."
}
```

```
"error": {
"name": "status_list_not_yet_valid",
"message": "The validFrom date on the status list credential is in the future."
}
```
And a fallback for any unknown error:
```
"error": {
"name": "status_list_error",
"message": "The status list couldn't be verified."
}
```

3. <b>fatal error</b>

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.
Expand Down
75 changes: 55 additions & 20 deletions src/Verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,24 @@ import { addTrustedIssuersToVerificationResponse } from './issuerRegistries.js';
import { addSchemaCheckToVerificationResponse } from './schemaCheck.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 {
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,
STATUS_LIST_EXPIRED,
UNKNOWN_STATUS_LIST_ERROR,
STATUS_LIST_SIGNATURE_ERROR,
STATUS_LIST_NOT_YET_VALID_ERROR,
STATUS_LIST_TYPE_ERROR
} 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 { EXPIRED_ERROR, ISSUER_DID_RESOLVES, NOT_FOUND_ERROR, STATUS_NOT_YET_VALID_ERROR, STATUS_SIGNATURE_ERROR, STATUS_TYPE_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';
import { GENERAL_STATUS_LIST_ERROR_MSG, STATUS_LIST_EXPIRED_MSG, STATUS_LIST_NOT_YET_VALID_MSG, STATUS_LIST_SIGNATURE_ERROR_MSG, STATUS_LIST_TYPE_ERROR_MSG } from './constants/messages.js';

const { purposes } = pkg;
const presentationPurpose = new purposes.AssertionProofPurpose();
Expand Down Expand Up @@ -80,7 +87,7 @@ export async function verifyPresentation({ presentation, challenge = 'meaningles
}


export async function verifyCredential({ credential, knownDIDRegistries}: { credential: Credential, knownDIDRegistries: object}): Promise<VerificationResponse> {
export async function verifyCredential({ credential, knownDIDRegistries }: { credential: Credential, knownDIDRegistries: object }): Promise<VerificationResponse> {
try {
// null unless credential has a status
const statusChecker = getCredentialStatusChecker(credential)
Expand All @@ -92,6 +99,7 @@ export async function verifyCredential({ credential, knownDIDRegistries}: { cred
checkStatus: statusChecker,
verifyMatchingIssuers: false
});

const adjustedResponse = await transformResponse(verificationResponse, credential, knownDIDRegistries)
return adjustedResponse;
} catch (error) {
Expand All @@ -107,7 +115,7 @@ async function transformResponse(verificationResponse: any, credential: Credenti
return fatalCredentialError
}

handleAnyStatusError({ verificationResponse, statusResult: verificationResponse.statusResult });
handleAnyStatusError({ verificationResponse });

const fatalSignatureError = handleAnySignatureError({ verificationResponse, credential })
if (fatalSignatureError) {
Expand All @@ -117,7 +125,7 @@ async function transformResponse(verificationResponse: any, credential: Credenti
const { issuer } = credential
await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, issuer })

await addSchemaCheckToVerificationResponse({verificationResponse, credential})
await addSchemaCheckToVerificationResponse({ verificationResponse, credential })

// remove things we don't need from the result or that are duplicated elsewhere
delete verificationResponse.results
Expand Down Expand Up @@ -171,21 +179,48 @@ function handleAnyFatalCredentialErrors(credential: Credential): VerificationRes
return null
}

function handleAnyStatusError({ verificationResponse }: {
verificationResponse: any,
statusResult: any
}): void {
function handleAnyStatusError({ verificationResponse }: { verificationResponse: any }): void {
const statusResult = verificationResponse.statusResult
if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) {
const statusStep = {
"id": REVOCATION_STATUS_STEP_ID,
"error": {
if (statusResult?.error) {
let error
if (statusResult?.error?.cause?.message?.startsWith(NOT_FOUND_ERROR)) {
error = {
name: STATUS_LIST_NOT_FOUND,
message: statusResult.error.cause.message
}
} else if (statusResult?.error?.cause?.message?.includes(EXPIRED_ERROR)) {
error = {
name: STATUS_LIST_EXPIRED,
message: STATUS_LIST_EXPIRED_MSG
}
} else if (statusResult?.error?.cause?.message?.startsWith(STATUS_SIGNATURE_ERROR)) {
error = {
name: STATUS_LIST_SIGNATURE_ERROR,
message: STATUS_LIST_SIGNATURE_ERROR_MSG
}
} else if (statusResult?.error?.message?.startsWith(STATUS_TYPE_ERROR)) {
error = {
name: STATUS_LIST_TYPE_ERROR,
message: STATUS_LIST_TYPE_ERROR_MSG
}
} else if (statusResult?.error?.cause?.message?.includes(STATUS_NOT_YET_VALID_ERROR)) {
error = {
name: STATUS_LIST_NOT_YET_VALID_ERROR,
message: STATUS_LIST_NOT_YET_VALID_MSG
}
} else {
error = {
name: UNKNOWN_STATUS_LIST_ERROR,
message: statusResult.error.cause?.message ?? GENERAL_STATUS_LIST_ERROR_MSG
}
}
const statusStep = {
"id": REVOCATION_STATUS_STEP_ID,
error
};
(verificationResponse.log ??= []).push(statusStep)
}

}

function handleAnySignatureError({ verificationResponse, credential }: { verificationResponse: any, credential: Credential }): null | VerificationResponse {
Expand All @@ -202,7 +237,7 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific
const httpError = verificationResponse.error.errors.find((error: any) => error.name === 'HTTPError')
// or a json-ld parsing error
const jsonLdError = verificationResponse.error.errors.find((error: any) => error.name === 'jsonld.ValidationError')

if (httpError) {
fatalErrorMessage = 'An http error prevented the signature check.'
errorName = HTTP_ERROR_WITH_SIGNATURE_CHECK
Expand All @@ -217,13 +252,13 @@ function handleAnySignatureError({ verificationResponse, credential }: { verific
}
}
} else if (jsonLdError) {
const errors = verificationResponse.error.errors.map((error:any)=>{
const errors = verificationResponse.error.errors.map((error: any) => {
// need to rename the stack property to stackTrace to fit with old error structure
error.stackTrace = error.stack;
delete error.stack;
return error
})
return {credential, errors}
return { credential, errors }
} else {
// not an http or json-ld error, so likely bad signature
fatalErrorMessage = 'The signature is not valid.'
Expand Down
8 changes: 7 additions & 1 deletion src/constants/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ 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'

export const STATUS_LIST_NOT_FOUND = 'status_list_not_found'
export const STATUS_LIST_EXPIRED = 'status_list_expired'
export const UNKNOWN_STATUS_LIST_ERROR = 'status_list_error'
export const STATUS_LIST_SIGNATURE_ERROR = 'status_list_signature_error'
export const STATUS_LIST_TYPE_ERROR = 'status_list_type_error'
export const STATUS_LIST_NOT_YET_VALID_ERROR = 'status_list_not_yet_valid'
6 changes: 5 additions & 1 deletion src/constants/external.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// 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'
export const EXPIRED_ERROR = 'is after "validUntil"'
export const VERIFICATION_ERROR = 'VerificationError'
export const STATUS_SIGNATURE_ERROR = 'Verification error'
export const STATUS_TYPE_ERROR = 'Status list credential type must include "BitstringStatusListCredential".'
export const STATUS_NOT_YET_VALID_ERROR = 'is before "validFrom"'
5 changes: 5 additions & 0 deletions src/constants/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const STATUS_LIST_EXPIRED_MSG = "The status list verifiable credential has expired."
export const STATUS_LIST_SIGNATURE_ERROR_MSG = "The signature on the status list is invalid."
export const STATUS_LIST_TYPE_ERROR_MSG = 'Status list credential type must include "BitstringStatusListCredential".'
export const STATUS_LIST_NOT_YET_VALID_MSG = 'The validFrom date on the status list credential is in the future.'
export const GENERAL_STATUS_LIST_ERROR_MSG = "The status list couldn't be verified."
13 changes: 7 additions & 6 deletions test/Verify.experimentation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,15 @@ describe('any test we like', () => {
it.skip('tests', async () => {
// change this however you like to test things
// const originalVC = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/legacyRegistry-noStatus-notExpired-withSchema.json')
const vc = testVC as any;
const notYetValidVC = await fetchVC('https://digitalcredentials.github.io/vc-test-fixtures/verifiableCredentials/v2/dataIntegrityProof/didKey/oidf-noStatus-notExpired-notYetValid.json')
// const vc = testVC as any;
// const vc = JSON.parse(JSON.stringify(originalVC))
// vc.proof = [vc.proof]
const result = await checkSchemas(vc)
// const result = await verifyCredential({ credential: didKeyCredential, knownDIDRegistries })
console.log(JSON.stringify(result, null, 2))
expect(result.results[0].result.errors).to.exist
expect(result.results[0].result.valid).to.be.false
// const result = await checkSchemas(vc)
const result = await verifyCredential({ credential: notYetValidVC, knownDIDRegistries })
// console.log(JSON.stringify(result, null, 2))
// expect(result.results[0].result.errors).to.exist
// expect(result.results[0].result.valid).to.be.false
})
})

Expand Down
Loading