diff --git a/src/Verify.ts b/src/Verify.ts index e6f3a46..a8f6eaf 100644 --- a/src/Verify.ts +++ b/src/Verify.ts @@ -2,7 +2,7 @@ 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 { verifyPresentation as vcVerifyPresentation, verifyCredential as vcVerifyCredential } from '@digitalcredentials/vc'; import { securityLoader } from '@digitalcredentials/security-document-loader'; import pkg from '@digitalcredentials/jsonld-signatures'; @@ -31,7 +31,6 @@ 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(); const documentLoader = securityLoader({ fetchRemoteContexts: true }).build(); @@ -51,24 +50,34 @@ export async function verifyPresentation({ presentation, challenge = 'meaningles reloadIssuerRegistry?: boolean } ): Promise { + const assertedPurpose = presentation?.proof?.proofPurpose; + + // TODO: AuthenticationProofPurpose({challenge}) ('authentication') is used by LCW and is correct, + // but the package currently seems to expect and verify presentations from systems that use + // assertionMethod. + const useAuthenticationPurpose = assertedPurpose === 'authenticationMethod' || assertedPurpose === 'authentication'; + const presentationPurpose = useAuthenticationPurpose + ? new purposes.AuthenticationProofPurpose({ challenge }) + : new purposes.AssertionProofPurpose(); + try { const credential = extractCredentialsFrom(presentation)?.find( vc => vc.credentialStatus); - const checkStatus = credential ? getCredentialStatusChecker(credential) : undefined; - const result = await vc.verify({ + const checkStatus = credential ? getCredentialStatusChecker(credential) : undefined; + const result = await vcVerifyPresentation({ presentation, presentationPurpose, suite, documentLoader, unsignedPresentation, checkStatus, - challenge, + challenge: challenge, verifyMatchingIssuers: false }); - const transformedCredentialResults = await Promise.all(result.credentialResults.map(async (credentialResult: any) => { + const transformedCredentialResults = await Promise.all(result.credentialResults?.map(async (credentialResult: any) => { return transformResponse(credentialResult, credentialResult.credential, knownDIDRegistries) - })); + }) ?? []); // take what we need from the presentation part of the result let signature: PresentationSignatureResult; @@ -92,7 +101,7 @@ export async function verifyCredential({ credential, knownDIDRegistries }: { cre // null unless credential has a status const statusChecker = getCredentialStatusChecker(credential) - const verificationResponse = await vc.verifyCredential({ + const verificationResponse = await vcVerifyCredential({ credential, suite, documentLoader, @@ -100,7 +109,7 @@ export async function verifyCredential({ credential, knownDIDRegistries }: { cre verifyMatchingIssuers: false }); - const adjustedResponse = await transformResponse(verificationResponse, credential, knownDIDRegistries) + const adjustedResponse = await transformResponse(verificationResponse, credential, knownDIDRegistries); return adjustedResponse; } catch (error) { return { errors: [{ message: 'Could not verify credential.', name: UNKNOWN_ERROR, stackTrace: error }] } diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 0c471b7..f30816f 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -1,6 +1,5 @@ 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'; diff --git a/src/types/@digitalcredentials/vc.d.ts b/src/types/@digitalcredentials/vc.d.ts new file mode 100644 index 0000000..bdd1957 --- /dev/null +++ b/src/types/@digitalcredentials/vc.d.ts @@ -0,0 +1,132 @@ +/** + * Type definitions for @digitalcredentials/vc + * + * These declarations override the JSDoc-inferred types from the JavaScript source. + * NOTE: The project has its own types in src/types/*.ts, so we keep these minimal + * to avoid conflicts while still providing proper function signatures. + */ + +// Use the project's own types or 'any' to avoid conflicts +type AnyCredential = any; +type AnyPresentation = any; +type AnyProof = any; +type AnySuite = any; +type AnyPurpose = any; +type AnyDocumentLoader = any; +type AnyStatusResult = any; + +// Status check function - permissive to accept () => boolean +export type CheckStatusFunction = (...args: any[]) => any; + +// Document loader +export type DocumentLoader = (url: string) => Promise; + +// Verification result types - permissive +export interface VerifyCredentialResult { + verified: boolean; + statusResult?: any; + results?: any[]; + error?: any; + log?: any[]; +} + +export interface VerifyPresentationResult { + verified: boolean; + presentationResult?: any; + credentialResults?: any[]; + error?: any; +} + +// Options interfaces with permissive types +export interface IssueOptions { + credential: AnyCredential; + suite: AnySuite | AnySuite[]; + purpose?: AnyPurpose; + documentLoader?: AnyDocumentLoader; + now?: string | Date; +} + +export interface DeriveOptions { + verifiableCredential: AnyCredential; + suite: AnySuite | AnySuite[]; + documentLoader?: AnyDocumentLoader; +} + +export interface VerifyOptions { + presentation?: AnyPresentation; + suite: AnySuite | AnySuite[]; + unsignedPresentation?: boolean; + presentationPurpose?: AnyPurpose; + challenge?: string | null; + controller?: string | object; + domain?: string; + documentLoader?: AnyDocumentLoader; + checkStatus?: CheckStatusFunction | null | undefined; + now?: string | Date; + verifyMatchingIssuers?: boolean; +} + +export interface VerifyCredentialOptions { + credential: AnyCredential; + suite: AnySuite | AnySuite[]; + purpose?: AnyPurpose; + documentLoader?: AnyDocumentLoader; + // TODO: undefined is preferred over null as it matches the JSDoc types + checkStatus?: CheckStatusFunction | null | undefined; + now?: string | Date; + verifyMatchingIssuers?: boolean; +} + +export interface CreatePresentationOptions { + verifiableCredential?: AnyCredential | AnyCredential[]; + id?: string; + holder?: string | { id: string }; + now?: string | Date; + version?: 'v1' | 'v2'; + verify?: boolean; +} + +export interface SignPresentationOptions { + presentation: AnyPresentation; + suite: AnySuite | AnySuite[]; + purpose?: AnyPurpose; + domain?: string; + challenge?: string; + documentLoader?: AnyDocumentLoader; +} + +// Proof purpose class +export class CredentialIssuancePurpose { + term: string; + controller?: string | object; + date?: string | Date; + maxTimestampDelta?: number; + + constructor(options?: { + controller?: string | object; + date?: string | Date; + maxTimestampDelta?: number; + }); + + validate(proof: AnyProof, options: any): Promise; +} + +// Main API Functions +export function issue(options: IssueOptions): Promise; +export function derive(options: DeriveOptions): Promise; +export function verify(options: VerifyOptions): Promise; +export function verifyPresentation(options: VerifyOptions): Promise; +export function verifyCredential(options: VerifyCredentialOptions): Promise; +export function createPresentation(options?: CreatePresentationOptions): AnyPresentation; +export function signPresentation(options: SignPresentationOptions): Promise; + +// Internal/Helper Functions +export function _checkPresentation(presentation: AnyPresentation): void; +export function _checkCredential(options: any): void; + +// Constants +export const defaultDocumentLoader: DocumentLoader; +export const dateRegex: RegExp; +export const contexts: Map; +export const CREDENTIALS_CONTEXT_V1_URL: string; +export const CREDENTIALS_CONTEXT_V2_URL: string; diff --git a/test/Verify.presentation.spec.ts b/test/Verify.presentation.spec.ts index 563ee3a..4b27d0d 100644 --- a/test/Verify.presentation.spec.ts +++ b/test/Verify.presentation.spec.ts @@ -41,8 +41,10 @@ import { getVCv1ExpiredWithValidStatus } from '../src/test-fixtures/vc.js' +import { purposes } from '@digitalcredentials/jsonld-signatures'; 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'; @@ -166,6 +168,35 @@ describe('Verify.verifyPresentation', () => { expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) }) + it('when presentation has proofPurpose authentication and matching challenge', async () => { + const challenge = 'auth-challenge-123' + const verifiableCredential = [v2WithStatus] + const presentation = await getSignedVP({ + holder, + verifiableCredential, + presentationPurpose: new purposes.AuthenticationProofPurpose({ challenge }), + challenge + }) as VerifiablePresentation + const credentialResults = [expectedV2WithStatusResult] + const expectedPresentationResult = getExpectedVerifiedPresentationResult({ credentialResults }) + const result = await verifyPresentation({ presentation, knownDIDRegistries, challenge }) + result.credentialResults?.forEach(credResult => delete credResult.additionalInformation) + expect(result).to.deep.equalInAnyOrder(expectedPresentationResult) + }) + + it('when presentation has proofPurpose authentication but wrong challenge', async () => { + const challenge = 'auth-challenge-123' + const verifiableCredential = [v2WithStatus] + const presentation = await getSignedVP({ + holder, + verifiableCredential, + presentationPurpose: new purposes.AuthenticationProofPurpose({ challenge }), + challenge + }) as VerifiablePresentation + const result = await verifyPresentation({ presentation, knownDIDRegistries, challenge: 'wrong-challenge' }) + expect(result?.presentationResult?.signature).to.equal(SIGNATURE_INVALID) + }) + }) describe('it returns as unverified', () => { diff --git a/test/vpUtils.ts b/test/vpUtils.ts index af2432e..009c83c 100644 --- a/test/vpUtils.ts +++ b/test/vpUtils.ts @@ -9,7 +9,6 @@ const documentLoader = securityLoader().build() import pkg from '@digitalcredentials/jsonld-signatures'; import { VerifiablePresentation } from '../src/types/presentation.js'; const { purposes } = pkg; -const presentationPurpose = new purposes.AssertionProofPurpose(); const key = await Ed25519VerificationKey2020.generate( { @@ -26,13 +25,31 @@ const key = await Ed25519VerificationKey2020.generate( 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 getSignedVP = async ({ + holder, + verifiableCredential, + presentationPurpose, + challenge = 'canbeanything33' +}: { + holder: string; + verifiableCredential?: any; + presentationPurpose?: any; + challenge?: string; +}): Promise => { + const presentation = createPresentation({ holder, verifiableCredential }); + + // TODO: AuthenticationProofPurpose({challenge}) ('authentication') is used by + // LCW and is correct, but the package currently seems to verify presentations + // from systems that use assertionMethod. + const purpose = presentationPurpose ?? new purposes.AssertionProofPurpose(); + return await signPresentation({ + presentation, + suite: signingSuite, + documentLoader, + challenge, + purpose + }); +}; export const getUnSignedVP = ({verifiableCredential}:{verifiableCredential?:any}):VerifiablePresentation => { return createPresentation({verifiableCredential}); diff --git a/tsconfig.esm.json b/tsconfig.esm.json index b9f3b51..33dd120 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -1,30 +1,9 @@ { + "extends": "./tsconfig.json", "compilerOptions": { - "strict": true, - "target": "es2022", - "lib": ["es2022", "dom"], - "module": "es2022", - "moduleResolution": "node", "outDir": "dist", "sourceMap": true, "declaration": true, - "declarationMap": true, - "noImplicitAny": true, - "removeComments": false, - "preserveConstEnums": true, - "baseUrl": ".", - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "resolveJsonModule": true - }, - "ts-node": { - "files": true - }, - "include": [ - "src/**/*", - ".eslintrc.js", - "karma.conf.js" - ], - "exclude": ["node_modules", "dist", "test", "src/test-fixtures"] + "declarationMap": true + } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e55ff51 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es2022", + "lib": ["es2022", "dom"], + "module": "es2022", + "moduleResolution": "node", + "outDir": "dist", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "noImplicitAny": true, + "removeComments": false, + "preserveConstEnums": true, + "baseUrl": ".", + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "resolveJsonModule": true, + "paths": { + "@digitalcredentials/vc": ["src/types/@digitalcredentials/vc.d.ts"] + } + }, + "ts-node": { + "files": true + }, + "include": [ + "src/**/*", + ".eslintrc.js", + "karma.conf.js" + ], + "exclude": ["node_modules", "dist", "test", "src/test-fixtures"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json index bb6faa6..4e752dc 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -12,7 +12,10 @@ "baseUrl": ".", "allowSyntheticDefaultImports": true, "skipLibCheck": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "paths": { + "@digitalcredentials/vc": ["src/types/@digitalcredentials/vc.d.ts"] + } }, "ts-node": { "files": true diff --git a/tsconfig.types.json b/tsconfig.types.json index 81a8ae5..a7f10da 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -15,7 +15,10 @@ "resolveJsonModule": true, "declaration": true, "emitDeclarationOnly": true, - "declarationDir": "dist" + "declarationDir": "dist", + "paths": { + "@digitalcredentials/vc": ["src/types/@digitalcredentials/vc.d.ts"] + } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test", "src/test-fixtures"]