UNPKG

@digitalcredentials/verifier-core

Version:

For verifying Verifiable Credentials in the browser, Node.js, and React Native.

238 lines 12.3 kB
// 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 { 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, 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 { 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 { 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(); 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 }) { 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) => { return transformResponse(credentialResult, credentialResult.credential, knownDIDRegistries); })); // take what we need from the presentation part of the result let signature; 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 }) { 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); return adjustedResponse; } catch (error) { return { errors: [{ message: 'Could not verify credential.', name: UNKNOWN_ERROR, stackTrace: error }] }; } } async function transformResponse(verificationResponse, credential, knownDIDRegistries) { const fatalCredentialError = handleAnyFatalCredentialErrors(credential); if (fatalCredentialError) { return fatalCredentialError; } handleAnyStatusError({ verificationResponse }); const fatalSignatureError = handleAnySignatureError({ verificationResponse, credential }); if (fatalSignatureError) { return fatalSignatureError; } const { issuer } = credential; await addTrustedIssuersToVerificationResponse({ verificationResponse, knownDIDRegistries, issuer }); await addSchemaCheckToVerificationResponse({ verificationResponse, credential }); // 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) => entry.id !== ISSUER_DID_RESOLVES); // add things we always want in the response verificationResponse.credential = credential; return verificationResponse; } function handleAnyFatalCredentialErrors(credential) { 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); } 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 }) { const statusResult = verificationResponse.statusResult; 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 }) { if (verificationResponse.error) { if (verificationResponse?.error?.name === VERIFICATION_ERROR) { // Can't verify the signature. Maybe a bad signature or a did:web that can't // be resolved or a json-ld error. 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 let fatalErrorMessage = ""; let errorName = ""; // check to see if the error is http related const httpError = verificationResponse.error.errors.find((error) => error.name === 'HTTPError'); // or a json-ld parsing error const jsonLdError = verificationResponse.error.errors.find((error) => error.name === 'jsonld.ValidationError'); 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 = ((credential.issuer).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}`; errorName = DID_WEB_UNRESOLVED; } } } else if (jsonLdError) { const errors = verificationResponse.error.errors.map((error) => { // 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 }; } else { // not an http or json-ld 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; } function buildFatalErrorObject(fatalErrorMessage, name, credential, stackTrace) { return { credential, errors: [{ name, message: fatalErrorMessage, ...(stackTrace ? { stackTrace } : null) }] }; } //# sourceMappingURL=Verify.js.map