From ee9158557221525f07ddfbda0b4d58511ec0c6a9 Mon Sep 17 00:00:00 2001 From: Maycon Mello Date: Thu, 2 Apr 2026 11:46:47 -0300 Subject: [PATCH 1/2] DCKA-3415: Filter out revoked and invalid credentials from default presentation --- .../default-presentation.test.ts | 95 ++++++++++++++++++- packages/core/src/verification-controller.ts | 41 ++++++-- 2 files changed, 124 insertions(+), 12 deletions(-) diff --git a/integration-tests/verification-flow/default-presentation.test.ts b/integration-tests/verification-flow/default-presentation.test.ts index c212339b..cf33f9b3 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,17 @@ const template3 = 'a3e775bb-aaab-4489-b31b-746dc74f76c5'; // 2 range proofs const template4 = '9b434ed1-3b65-4b7c-b678-afc7e218f063'; +// Create tampered credentials for invalid/revoked status tests +const invalidCredentialId = 'https://creds-staging.truvera.io/invalid-university-degree'; +const revokedCredentialId = 'https://creds-staging.truvera.io/revoked-university-degree'; + +function createTamperedCredential(base: any, newId: string) { + return { + ...JSON.parse(JSON.stringify(base)), + id: newId, + }; +} + let wallet: IWallet; let didProvider; @@ -37,6 +49,27 @@ 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); + + await credentialProvider.addCredential(invalidCredential); + await credentialProvider.addCredential(revokedCredential); + + // 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 +216,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 +393,64 @@ 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 and revoked 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 and revoked 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); + expect(hasInvalid).toBe(true); + expect(hasRevoked).toBe(true); + + const presentation = await controller.createDefaultPresentation(); + + expect(presentation).toBeDefined(); + expect(presentation.verifiableCredential).toBeDefined(); + + // The selected credentials should NOT include invalid or revoked ones + const selectedIds = [...controller.selectedCredentials.keys()]; + expect(selectedIds).not.toContain(invalidCredentialId); + expect(selectedIds).not.toContain(revokedCredentialId); + }); + + it('should filter out invalid and revoked 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 or revoked credentials + const alternativeIds = options.alternatives.map(c => c.id); + expect(alternativeIds).not.toContain(invalidCredentialId); + expect(alternativeIds).not.toContain(revokedCredentialId); }); }); diff --git a/packages/core/src/verification-controller.ts b/packages/core/src/verification-controller.ts index 905f2534..fd1916b9 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,26 @@ 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, + ) + .map(r => r.credential); + } + async function createDefaultPresentation() { assert(filteredCredentials.length > 0, 'No filtered credentials available'); @@ -269,9 +290,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 +326,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 +338,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 +358,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 +684,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; From fadb46d46ca0bac6a10a3dab8a1323329096195c Mon Sep 17 00:00:00 2001 From: Maycon Mello Date: Thu, 2 Apr 2026 11:54:58 -0300 Subject: [PATCH 2/2] DCKA-3415: Also filter out expired credentials from default presentation --- .../default-presentation.test.ts | 24 +++++++++++++------ packages/core/src/verification-controller.ts | 3 ++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/integration-tests/verification-flow/default-presentation.test.ts b/integration-tests/verification-flow/default-presentation.test.ts index cf33f9b3..4287f8e1 100644 --- a/integration-tests/verification-flow/default-presentation.test.ts +++ b/integration-tests/verification-flow/default-presentation.test.ts @@ -24,14 +24,16 @@ const template3 = 'a3e775bb-aaab-4489-b31b-746dc74f76c5'; // 2 range proofs const template4 = '9b434ed1-3b65-4b7c-b678-afc7e218f063'; -// Create tampered credentials for invalid/revoked status tests +// 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) { +function createTamperedCredential(base: any, newId: string, overrides: any = {}) { return { ...JSON.parse(JSON.stringify(base)), id: newId, + ...overrides, }; } @@ -53,9 +55,13 @@ describe('Default presentation', () => { // 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)); @@ -398,7 +404,7 @@ describe('Default presentation', () => { expect(await controller.canSwitchCredential('non-existent-id')).toBe(false); }); - it('should filter out invalid and revoked credentials from default presentation', async () => { + it('should filter out invalid, revoked and expired credentials from default presentation', async () => { const proofRequest = await createProofRequest(template1); const controller = createVerificationController({ @@ -410,26 +416,29 @@ describe('Default presentation', () => { template: proofRequest, }); - // The filtered credentials should include the invalid and revoked ones + // 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 or revoked ones + // 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 and revoked credentials from switch alternatives', async () => { + it('should filter out invalid, revoked and expired credentials from switch alternatives', async () => { const proofRequest = await createProofRequest(template1); const controller = createVerificationController({ @@ -448,9 +457,10 @@ describe('Default presentation', () => { const options = await controller.getCredentialOptionsForDescriptor(selectedCredentialId); - // Alternatives should not contain invalid or revoked credentials + // 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 fd1916b9..41f71f35 100644 --- a/packages/core/src/verification-controller.ts +++ b/packages/core/src/verification-controller.ts @@ -278,7 +278,8 @@ export function createVerificationController({ .filter( r => r.status !== CredentialStatus.Revoked && - r.status !== CredentialStatus.Invalid, + r.status !== CredentialStatus.Invalid && + r.status !== CredentialStatus.Expired, ) .map(r => r.credential); }