Skip to content
Open
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
27 changes: 18 additions & 9 deletions src/Verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();

Expand All @@ -51,24 +50,34 @@ export async function verifyPresentation({ presentation, challenge = 'meaningles
reloadIssuerRegistry?: boolean
}
): Promise<PresentationVerificationResponse> {
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;
Expand All @@ -92,15 +101,15 @@ 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,
checkStatus: statusChecker,
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 }] }
Expand Down
1 change: 0 additions & 1 deletion src/declarations.d.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
132 changes: 132 additions & 0 deletions src/types/@digitalcredentials/vc.d.ts
Original file line number Diff line number Diff line change
@@ -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<any>;

// 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<boolean>;
}

// Main API Functions
export function issue(options: IssueOptions): Promise<AnyCredential>;
export function derive(options: DeriveOptions): Promise<AnyCredential>;
export function verify(options: VerifyOptions): Promise<VerifyPresentationResult>;
export function verifyPresentation(options: VerifyOptions): Promise<VerifyPresentationResult>;
export function verifyCredential(options: VerifyCredentialOptions): Promise<VerifyCredentialResult>;
export function createPresentation(options?: CreatePresentationOptions): AnyPresentation;
export function signPresentation(options: SignPresentationOptions): Promise<AnyPresentation>;

// 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<string, object>;
export const CREDENTIALS_CONTEXT_V1_URL: string;
export const CREDENTIALS_CONTEXT_V2_URL: string;
31 changes: 31 additions & 0 deletions test/Verify.presentation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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', () => {
Expand Down
33 changes: 25 additions & 8 deletions test/vpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand All @@ -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<any> => {
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<any> => {
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});
Expand Down
27 changes: 3 additions & 24 deletions tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -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
}
}
33 changes: 33 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
5 changes: 4 additions & 1 deletion tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading