diff --git a/integration-tests/verification-flow/default-presentation.test.ts b/integration-tests/verification-flow/default-presentation.test.ts index c212339b..4287f8e1 100644 --- a/integration-tests/verification-flow/default-presentation.test.ts +++ b/integration-tests/verification-flow/default-presentation.test.ts @@ -6,6 +6,7 @@ import { getCredentialProvider, } from '../helpers/wallet-helpers'; import {createVerificationController} from '@docknetwork/wallet-sdk-core/src/verification-controller'; +import {CredentialStatus} from '@docknetwork/wallet-sdk-core/src/credential-provider'; import universityDegree from '../data/default-presentation-tests/university-degree.json'; import universityDegree2 from '../data/default-presentation-tests/university-degree-2.json'; import equinetCreditScore from '../data/default-presentation-tests/equinet-credit-score.json'; @@ -23,6 +24,19 @@ const template3 = 'a3e775bb-aaab-4489-b31b-746dc74f76c5'; // 2 range proofs const template4 = '9b434ed1-3b65-4b7c-b678-afc7e218f063'; +// Create tampered credentials for invalid/revoked/expired status tests +const invalidCredentialId = 'https://creds-staging.truvera.io/invalid-university-degree'; +const revokedCredentialId = 'https://creds-staging.truvera.io/revoked-university-degree'; +const expiredCredentialId = 'https://creds-staging.truvera.io/expired-university-degree'; + +function createTamperedCredential(base: any, newId: string, overrides: any = {}) { + return { + ...JSON.parse(JSON.stringify(base)), + id: newId, + ...overrides, + }; +} + let wallet: IWallet; let didProvider; @@ -37,6 +51,31 @@ describe('Default presentation', () => { await credentialProvider.addCredential(universityDegree); await credentialProvider.addCredential(universityDegree2); await credentialProvider.addCredential(equinetCreditScore); + + // Add tampered credentials and force their cached status + const invalidCredential = createTamperedCredential(universityDegree, invalidCredentialId); + const revokedCredential = createTamperedCredential(universityDegree, revokedCredentialId); + const expiredCredential = createTamperedCredential(universityDegree, expiredCredentialId, { + expirationDate: '2020-01-01T00:00:00Z', + }); + + await credentialProvider.addCredential(invalidCredential); + await credentialProvider.addCredential(revokedCredential); + await credentialProvider.addCredential(expiredCredential); + + // Wait for the background status sync triggered by addCredential to settle + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Overwrite the cached status docs to simulate invalid and revoked states + const invalidStatusDoc = await wallet.getDocumentById(`${invalidCredentialId}#status`); + invalidStatusDoc.status = CredentialStatus.Invalid; + invalidStatusDoc.error = 'Credential verification failed'; + await wallet.updateDocument(invalidStatusDoc); + + const revokedStatusDoc = await wallet.getDocumentById(`${revokedCredentialId}#status`); + revokedStatusDoc.status = CredentialStatus.Revoked; + revokedStatusDoc.error = 'Credential has been revoked'; + await wallet.updateDocument(revokedStatusDoc); }); afterAll(() => closeWallet()); @@ -183,7 +222,7 @@ describe('Default presentation', () => { const descriptors = controller.getSelectedCredentialsByDescriptor(); const selectedCredentialId = descriptors[0].selected.id; - const options = controller.getCredentialOptionsForDescriptor(selectedCredentialId); + const options = await controller.getCredentialOptionsForDescriptor(selectedCredentialId); expect(options.selected.id).toBe(selectedCredentialId); expect(options.alternatives.length).toBeGreaterThanOrEqual(1); @@ -360,8 +399,68 @@ describe('Default presentation', () => { const selectedCredentialId = descriptors[0].selected.id; // Template1 has 2 matching university degrees, so switching should be possible - expect(controller.canSwitchCredential(selectedCredentialId)).toBe(true); + expect(await controller.canSwitchCredential(selectedCredentialId)).toBe(true); // Non-existent credential should return false - expect(controller.canSwitchCredential('non-existent-id')).toBe(false); + expect(await controller.canSwitchCredential('non-existent-id')).toBe(false); + }); + + it('should filter out invalid, revoked and expired credentials from default presentation', async () => { + const proofRequest = await createProofRequest(template1); + + const controller = createVerificationController({ + wallet, + didProvider, + }); + + await controller.start({ + template: proofRequest, + }); + + // The filtered credentials should include the invalid, revoked and expired ones + // since PEX filtering only checks schema/type match, not status + const allFiltered = controller.getFilteredCredentials(); + const hasInvalid = allFiltered.some(c => c.id === invalidCredentialId); + const hasRevoked = allFiltered.some(c => c.id === revokedCredentialId); + const hasExpired = allFiltered.some(c => c.id === expiredCredentialId); + expect(hasInvalid).toBe(true); + expect(hasRevoked).toBe(true); + expect(hasExpired).toBe(true); + + const presentation = await controller.createDefaultPresentation(); + + expect(presentation).toBeDefined(); + expect(presentation.verifiableCredential).toBeDefined(); + + // The selected credentials should NOT include invalid, revoked or expired ones + const selectedIds = [...controller.selectedCredentials.keys()]; + expect(selectedIds).not.toContain(invalidCredentialId); + expect(selectedIds).not.toContain(revokedCredentialId); + expect(selectedIds).not.toContain(expiredCredentialId); + }); + + it('should filter out invalid, revoked and expired credentials from switch alternatives', async () => { + const proofRequest = await createProofRequest(template1); + + const controller = createVerificationController({ + wallet, + didProvider, + }); + + await controller.start({ + template: proofRequest, + }); + + await controller.createDefaultPresentation(); + + const descriptors = controller.getSelectedCredentialsByDescriptor(); + const selectedCredentialId = descriptors[0].selected.id; + + const options = await controller.getCredentialOptionsForDescriptor(selectedCredentialId); + + // Alternatives should not contain invalid, revoked or expired credentials + const alternativeIds = options.alternatives.map(c => c.id); + expect(alternativeIds).not.toContain(invalidCredentialId); + expect(alternativeIds).not.toContain(revokedCredentialId); + expect(alternativeIds).not.toContain(expiredCredentialId); }); }); diff --git a/packages/core/src/verification-controller.ts b/packages/core/src/verification-controller.ts index 905f2534..41f71f35 100644 --- a/packages/core/src/verification-controller.ts +++ b/packages/core/src/verification-controller.ts @@ -3,6 +3,7 @@ import {pexService} from '@docknetwork/wallet-sdk-wasm/src/services/pex'; import {credentialServiceRPC} from '@docknetwork/wallet-sdk-wasm/src/services/credential'; import { createCredentialProvider, + CredentialStatus, ICredentialProvider, } from './credential-provider'; import {IWallet} from './types'; @@ -59,14 +60,14 @@ export interface IVerificationController { candidates: any[]; }>; getSelectedCredentialsByDescriptor: () => any[]; - getCredentialOptionsForDescriptor: (credentialId: string) => any; + getCredentialOptionsForDescriptor: (credentialId: string) => Promise; switchCredential: ( currentCredentialId: string, replacementCredentialId: string, ) => Promise; getRequestedAttributes: (credentialId: string) => any[]; getCredentialStatus: (credentialId: string) => Promise; - canSwitchCredential: (credentialId: string) => boolean; + canSwitchCredential: (credentialId: string) => Promise; getTemplateJSON: () => any; } @@ -262,6 +263,27 @@ export function createVerificationController({ return groups; } + async function filterValidCredentials(candidates: any[]) { + const results = await Promise.all( + candidates.map(async cred => { + const statusResult = await credentialProvider.getCredentialStatus(cred); + return { + credential: cred, + status: statusResult.status, + }; + }), + ); + + return results + .filter( + r => + r.status !== CredentialStatus.Revoked && + r.status !== CredentialStatus.Invalid && + r.status !== CredentialStatus.Expired, + ) + .map(r => r.credential); + } + async function createDefaultPresentation() { assert(filteredCredentials.length > 0, 'No filtered credentials available'); @@ -269,9 +291,10 @@ export function createVerificationController({ const groups = getRequirementGroups(); for (const group of groups) { + const validCandidates = await filterValidCredentials(group.candidates); const chosen = - group.candidates.find(cred => !selectedCredentials.has(cred.id)) || - group.candidates[0]; + validCandidates.find(cred => !selectedCredentials.has(cred.id)) || + validCandidates[0]; if (chosen) { selectedCredentials.set(chosen.id, {credential: chosen}); } @@ -304,7 +327,7 @@ export function createVerificationController({ }); } - function getCredentialOptionsForDescriptor(credentialId: string) { + async function getCredentialOptionsForDescriptor(credentialId: string) { const groups = getRequirementGroups(); const group = groups.find(g => g.candidates.some(c => c.id === credentialId), @@ -316,7 +339,8 @@ export function createVerificationController({ ); const selected = group.candidates.find(c => c.id === credentialId); - const alternatives = group.candidates.filter(c => c.id !== credentialId); + const allAlternatives = group.candidates.filter(c => c.id !== credentialId); + const alternatives = await filterValidCredentials(allAlternatives); return { descriptorId: group.descriptorKey, @@ -335,7 +359,7 @@ export function createVerificationController({ `Credential ${currentCredentialId} is not currently selected`, ); - const options = getCredentialOptionsForDescriptor(currentCredentialId); + const options = await getCredentialOptionsForDescriptor(currentCredentialId); const replacement = options.alternatives.find( c => c.id === replacementCredentialId, ); @@ -661,9 +685,9 @@ export function createVerificationController({ return credentialProvider.isValid(credential); } - function canSwitchCredential(credentialId: string) { + async function canSwitchCredential(credentialId: string) { try { - const options = getCredentialOptionsForDescriptor(credentialId); + const options = await getCredentialOptionsForDescriptor(credentialId); return options.alternatives.length > 0; } catch { return false;