From 2763f700d87ed0e92e046ed276a1068043c5d89e Mon Sep 17 00:00:00 2001 From: Maycon Mello Date: Fri, 6 Mar 2026 14:21:16 -0300 Subject: [PATCH 1/4] Use credential-sdk to generation presentation for pex request --- .../verification-flow/range-proofs.test.ts | 129 ++++++++++++++++++ packages/core/src/verification-controller.ts | 109 +++++++++++++++ .../wasm/src/services/credential/config.ts | 7 + .../src/services/credential/service-rpc.js | 3 + .../wasm/src/services/credential/service.ts | 82 +++++++++++ 5 files changed, 330 insertions(+) diff --git a/integration-tests/verification-flow/range-proofs.test.ts b/integration-tests/verification-flow/range-proofs.test.ts index 9e56ed95..5e96542a 100644 --- a/integration-tests/verification-flow/range-proofs.test.ts +++ b/integration-tests/verification-flow/range-proofs.test.ts @@ -157,5 +157,134 @@ describe('Range proofs verification', () => { credential.credentialSubject.salary, ); }); + it('V2: should create range proofs presentation without subject', async () => { + const wallet: IWallet = await getWallet(); + const controller = await createVerificationController({ + wallet, + }); + + const credentialUrl = + 'https://creds-testnet.dock.io/697fe144364680179937fa748a5b2483f3ee2743ab751cbad8305338eb53a7a3'; + const password = '1234'; + const {data: credential} = await axios.get( + `${credentialUrl}?p=${btoa(password)}`, + ); + + await addCredentialIfNotExists(credential); + + await controller.start({ + template: proofRequest.qr, + }); + + const attributesToReveal = []; + + controller.selectedCredentials.set(credential.id, { + credential: credential, + attributesToReveal, + }); + + const presentationV1 = await controller.createPresentation(); + const presentationV2 = await controller.createPresentationV2(); + + expect( + presentationV2.verifiableCredential[0].credentialSubject, + ).toBeUndefined(); + + expect( + presentationV2.verifiableCredential[0].proof.bounds.credentialSubject + .dateOfBirth, + ).toEqual( + presentationV1.verifiableCredential[0].proof.bounds.credentialSubject + .dateOfBirth, + ); + }); + + it('V2: should not reveal range proof attributes for KVAC credentials', async () => { + const wallet: IWallet = await getWallet(); + const controller = await createVerificationController({ + wallet, + }); + + const credentialUrl = + 'https://creds-testnet.truvera.io/16c431993e6678af4fc74a9b995250bfe3a2ecf215baa31f0945164ac89fd798'; + const password = 'test'; + const {data: credential} = await axios.get( + `${credentialUrl}?p=${btoa(password)}`, + ); + + await addCredentialIfNotExists(credential); + + await controller.start({ + template: proofRequest.qr, + }); + + const attributesToReveal = ['credentialSubject.id']; + + controller.selectedCredentials.set(credential.id, { + credential: credential, + attributesToReveal, + }); + + const presentationV1 = await controller.createPresentation(); + const presentationV2 = await controller.createPresentationV2(); + + expect( + presentationV2.verifiableCredential[0].credentialSubject.dateOfBirth, + ).toBeUndefined(); + expect( + presentationV2.verifiableCredential[0].credentialSubject.id, + ).toBeDefined(); + + expect( + presentationV2.verifiableCredential[0].proof.bounds.credentialSubject + .dateOfBirth, + ).toEqual( + presentationV1.verifiableCredential[0].proof.bounds.credentialSubject + .dateOfBirth, + ); + }); + + it('V2: should not reveal issuanceDate', async () => { + const wallet: IWallet = await getWallet(); + const controller = await createVerificationController({ + wallet, + }); + + await controller.start({ + template: proofRequest.qr, + }); + + const credentialUrl = + 'https://creds-testnet.dock.io/697fe144364680179937fa748a5b2483f3ee2743ab751cbad8305338eb53a7a3'; + const password = '1234'; + const {data: credential} = await axios.get( + `${credentialUrl}?p=${btoa(password)}`, + ); + + await addCredentialIfNotExists(credential); + + const attributesToReveal = ['issuanceDate', 'credentialSubject.salary']; + + controller.selectedCredentials.set(credential.id, { + credential: credential, + attributesToReveal, + }); + + const presentationV1 = await controller.createPresentation(); + const presentationV2 = await controller.createPresentationV2(); + + expect(presentationV2.verifiableCredential[0].issuanceDate).not.toBe( + credential.issuanceDate, + ); + expect(presentationV2.verifiableCredential[0].credentialSubject.salary).toBe( + credential.credentialSubject.salary, + ); + + // V2 should match V1 behavior + expect(presentationV2.verifiableCredential[0].issuanceDate).not.toBe( + presentationV1.verifiableCredential[0].issuanceDate, + ); + }); + afterAll(() => closeWallet()); }); diff --git a/packages/core/src/verification-controller.ts b/packages/core/src/verification-controller.ts index 3ec9033d..734b9de9 100644 --- a/packages/core/src/verification-controller.ts +++ b/packages/core/src/verification-controller.ts @@ -221,6 +221,114 @@ export function createVerificationController({ return presentation; } + async function createPresentationV2() { + assert(!!selectedDID, 'No DID selected'); + assert(!!selectedCredentials.size, 'No credentials selected'); + + const didKeyPairList = await didProvider.getDIDKeyPairs(); + const keyDoc = didKeyPairList.find(doc => doc.controller === selectedDID); + assert(keyDoc, `No key pair found for the selected DID ${selectedDID}`); + + const sdJwtSelections = []; + const bbsKvacSelections = []; + const regularSelections = []; + + for (const credentialSelection of selectedCredentials.values()) { + if (credentialSelection.credential._sd_jwt) { + sdJwtSelections.push(credentialSelection); + } else { + const isBBS = await isBBSPlusCredential(credentialSelection.credential); + const isKVAC = await isKvacCredential(credentialSelection.credential); + if (isBBS || isKVAC) { + bbsKvacSelections.push(credentialSelection); + } else { + regularSelections.push(credentialSelection); + } + } + } + + if (bbsKvacSelections.length > 0) { + const credentialsWithWitness = await Promise.all( + bbsKvacSelections.map(async sel => ({ + credential: sel.credential, + witness: await credentialProvider.getMembershipWitness(sel.credential.id), + attributesToReveal: sel.attributesToReveal || [], + })), + ); + + if (sdJwtSelections.length === 0 && regularSelections.length === 0) { + return credentialServiceRPC.generatePresentationFromPex({ + credentials: credentialsWithWitness, + pexRequest: templateJSON.request, + holderKeyDoc: keyDoc, + holderDid: selectedDID, + challenge: templateJSON.nonce, + domain: 'dock.io', + boundCheckSnarkKey: templateJSON.boundCheckSnarkKey, + skipSigning: true, + }); + } + + const derivedCredentials = + await credentialServiceRPC.deriveVCFromPresentation({ + proofRequest: templateJSON, + credentials: credentialsWithWitness.map(c => ({ + credential: c.credential, + witness: c.witness, + attributesToReveal: [...(c.attributesToReveal || []), 'id'], + })), + }); + + const allCredentials = [...derivedCredentials]; + + for (const sel of sdJwtSelections) { + const derived = await credentialServiceRPC.createSDJWTPresentation({ + attributesToReveal: sel.attributesToReveal, + credential: sel.credential._sd_jwt.encoded, + }); + allCredentials.push(derived); + } + + for (const sel of regularSelections) { + allCredentials.push(sel.credential); + } + + return credentialServiceRPC.createPresentation({ + credentials: allCredentials, + challenge: templateJSON.nonce, + keyDoc, + id: keyDoc.controller.startsWith('did:key:') + ? keyDoc.id + : `${keyDoc.controller}#keys-1`, + domain: 'dock.io', + }); + } + + const credentials = []; + + for (const sel of sdJwtSelections) { + const derived = await credentialServiceRPC.createSDJWTPresentation({ + attributesToReveal: sel.attributesToReveal, + credential: sel.credential._sd_jwt.encoded, + }); + credentials.push(derived); + } + + for (const sel of regularSelections) { + credentials.push(sel.credential); + } + + return credentialServiceRPC.createPresentation({ + credentials, + challenge: templateJSON.nonce, + keyDoc, + id: keyDoc.controller.startsWith('did:key:') + ? keyDoc.id + : `${keyDoc.controller}#keys-1`, + domain: 'dock.io', + }); + } + /** * Filtered credentials */ @@ -280,6 +388,7 @@ export function createVerificationController({ loadCredentials, getFilteredCredentials, createPresentation, + createPresentationV2, evaluatePresentation, getTemplateJSON() { return templateJSON; diff --git a/packages/wasm/src/services/credential/config.ts b/packages/wasm/src/services/credential/config.ts index 1b0185ab..cbbd5583 100644 --- a/packages/wasm/src/services/credential/config.ts +++ b/packages/wasm/src/services/credential/config.ts @@ -40,6 +40,13 @@ export const validation = { 'publicKeyBase58 is not present', ); }, + generatePresentationFromPex: params => { + const {credentials, pexRequest, challenge} = params; + assert(Array.isArray(credentials), 'invalid credentials'); + assert(credentials.length > 0, 'no credential found'); + assert(pexRequest, 'pexRequest is required'); + assert(challenge, 'challenge is required'); + }, createPresentation: params => { const {credentials, keyDoc, challenge, id} = params; assert(typeof id === 'string', 'invalid id'); diff --git a/packages/wasm/src/services/credential/service-rpc.js b/packages/wasm/src/services/credential/service-rpc.js index 6b17b005..4dd1bf48 100644 --- a/packages/wasm/src/services/credential/service-rpc.js +++ b/packages/wasm/src/services/credential/service-rpc.js @@ -44,4 +44,7 @@ export class CredentialServiceRPC extends RpcService { async acquireOIDCredential(params) { return this.call('acquireOIDCredential', params); } + async generatePresentationFromPex(params) { + return this.call('generatePresentationFromPex', params); + } } diff --git a/packages/wasm/src/services/credential/service.ts b/packages/wasm/src/services/credential/service.ts index 92ba0e4b..66332ef0 100644 --- a/packages/wasm/src/services/credential/service.ts +++ b/packages/wasm/src/services/credential/service.ts @@ -33,7 +33,15 @@ import { applyEnforceBounds, hasProvingKey, fetchProvingKey, + isBase64OrDataUrl, + blobFromBase64, + fetchBlobFromUrl, } from './bound-check'; +import { + generatePresentationFromPexRequest, + GeneratePresentationStatus, +} from '@docknetwork/credential-sdk/pex'; +import {LegoProvingKey} from '@docknetwork/crypto-wasm-ts/lib/legosnark'; import assert from 'assert'; import axios from 'axios'; import {getIsRevoked, getWitnessDetails} from './bbs-revocation'; @@ -122,6 +130,7 @@ class CredentialService { CredentialService.prototype.credentialToW3C, CredentialService.prototype.createSDJWTPresentation, CredentialService.prototype.acquireOIDCredential, + CredentialService.prototype.generatePresentationFromPex, ]; @@ -739,6 +748,79 @@ class CredentialService { return credentialsFromPresentation; } + async generatePresentationFromPex(params) { + validation.generatePresentationFromPex(params); + const { + credentials, + pexRequest, + holderKeyDoc, + holderDid, + challenge, + domain, + boundCheckSnarkKey, + skipSigning, + } = params; + + const resolvedWitnesses = await Promise.all( + credentials.map(async (c) => { + if (!c.witness) { + return undefined; + } + const details = await getWitnessDetails(c.credential, c.witness); + const chainModule = + c.credential.credentialStatus.id.indexOf('dock:accumulator') === 0 + ? blockchainService.modules.accumulator.modules[0] + : blockchainService.modules.accumulator.modules[ + blockchainService.modules.accumulator.modules.length - 1 + ]; + const accumulatorModuleClass = chainModule.constructor; + return { + membershipWitness: details.membershipWitness, + accumulated: accumulatorModuleClass.accumulatedFromHex( + details.accumulator.accumulated, + AccumulatorType.VBPos, + ), + pk: details.pk, + params: details.params, + }; + }), + ); + + const result = await generatePresentationFromPexRequest({ + credentials: credentials.map(c => c.credential), + pexRequest, + holderKeyDoc, + holderDid, + challenge, + domain, + resolver: blockchainService.resolver, + skipSigning: skipSigning || false, + loadProvingKey: boundCheckSnarkKey + ? async () => { + const blob = (await isBase64OrDataUrl(boundCheckSnarkKey)) + ? blobFromBase64(boundCheckSnarkKey) + : await fetchBlobFromUrl(boundCheckSnarkKey); + return { + provingKey: new LegoProvingKey(blob), + provingKeyId: 'key0', + }; + } + : undefined, + selectiveDisclosure: { + credentials: credentials.map((c, i) => ({ + attributes: [...(c.attributesToReveal || []), 'id'], + witness: resolvedWitnesses[i], + })), + }, + }); + + if (result.status !== GeneratePresentationStatus.SUCCESS) { + throw result.error || new Error(`Presentation generation failed: ${result.status}`); + } + + return result.presentation; + } + /** * Test method for range proofs * @private From 82091f892f041179218842b0175615252c1c6d78 Mon Sep 17 00:00:00 2001 From: Maycon Mello Date: Fri, 6 Mar 2026 14:30:00 -0300 Subject: [PATCH 2/4] Use credential-sdk to generation presentation for pex request --- .../verification-flow/range-proofs.test.ts | 135 +----------------- packages/core/src/verification-controller.ts | 91 ------------ 2 files changed, 1 insertion(+), 225 deletions(-) diff --git a/integration-tests/verification-flow/range-proofs.test.ts b/integration-tests/verification-flow/range-proofs.test.ts index 5e96542a..1d6814cf 100644 --- a/integration-tests/verification-flow/range-proofs.test.ts +++ b/integration-tests/verification-flow/range-proofs.test.ts @@ -35,10 +35,6 @@ describe('Range proofs verification', () => { template: proofRequest.qr, }); - // We can keep the attributes to reveal empty - // Range proof attributes will be handled automatically during presentation creation - // and will not be revealed in the presentation - // even if we include them in the attributesToReveal, they will be ignored const attributesToReveal = []; controller.selectedCredentials.set(credential.id, { @@ -149,7 +145,7 @@ describe('Range proofs verification', () => { const presentation = await controller.createPresentation(); // Presentation issuanceDate should not be equal to the credential issuanceDate - // The credential SDK will genreate a presentation timestamp instead + // The credential SDK will generate a presentation timestamp instead expect(presentation.verifiableCredential[0].issuanceDate).not.toBe( credential.issuanceDate, ); @@ -157,134 +153,5 @@ describe('Range proofs verification', () => { credential.credentialSubject.salary, ); }); - it('V2: should create range proofs presentation without subject', async () => { - const wallet: IWallet = await getWallet(); - const controller = await createVerificationController({ - wallet, - }); - - const credentialUrl = - 'https://creds-testnet.dock.io/697fe144364680179937fa748a5b2483f3ee2743ab751cbad8305338eb53a7a3'; - const password = '1234'; - const {data: credential} = await axios.get( - `${credentialUrl}?p=${btoa(password)}`, - ); - - await addCredentialIfNotExists(credential); - - await controller.start({ - template: proofRequest.qr, - }); - - const attributesToReveal = []; - - controller.selectedCredentials.set(credential.id, { - credential: credential, - attributesToReveal, - }); - - const presentationV1 = await controller.createPresentation(); - const presentationV2 = await controller.createPresentationV2(); - - expect( - presentationV2.verifiableCredential[0].credentialSubject, - ).toBeUndefined(); - - expect( - presentationV2.verifiableCredential[0].proof.bounds.credentialSubject - .dateOfBirth, - ).toEqual( - presentationV1.verifiableCredential[0].proof.bounds.credentialSubject - .dateOfBirth, - ); - }); - - it('V2: should not reveal range proof attributes for KVAC credentials', async () => { - const wallet: IWallet = await getWallet(); - const controller = await createVerificationController({ - wallet, - }); - - const credentialUrl = - 'https://creds-testnet.truvera.io/16c431993e6678af4fc74a9b995250bfe3a2ecf215baa31f0945164ac89fd798'; - const password = 'test'; - const {data: credential} = await axios.get( - `${credentialUrl}?p=${btoa(password)}`, - ); - - await addCredentialIfNotExists(credential); - - await controller.start({ - template: proofRequest.qr, - }); - - const attributesToReveal = ['credentialSubject.id']; - - controller.selectedCredentials.set(credential.id, { - credential: credential, - attributesToReveal, - }); - - const presentationV1 = await controller.createPresentation(); - const presentationV2 = await controller.createPresentationV2(); - - expect( - presentationV2.verifiableCredential[0].credentialSubject.dateOfBirth, - ).toBeUndefined(); - expect( - presentationV2.verifiableCredential[0].credentialSubject.id, - ).toBeDefined(); - - expect( - presentationV2.verifiableCredential[0].proof.bounds.credentialSubject - .dateOfBirth, - ).toEqual( - presentationV1.verifiableCredential[0].proof.bounds.credentialSubject - .dateOfBirth, - ); - }); - - it('V2: should not reveal issuanceDate', async () => { - const wallet: IWallet = await getWallet(); - const controller = await createVerificationController({ - wallet, - }); - - await controller.start({ - template: proofRequest.qr, - }); - - const credentialUrl = - 'https://creds-testnet.dock.io/697fe144364680179937fa748a5b2483f3ee2743ab751cbad8305338eb53a7a3'; - const password = '1234'; - const {data: credential} = await axios.get( - `${credentialUrl}?p=${btoa(password)}`, - ); - - await addCredentialIfNotExists(credential); - - const attributesToReveal = ['issuanceDate', 'credentialSubject.salary']; - - controller.selectedCredentials.set(credential.id, { - credential: credential, - attributesToReveal, - }); - - const presentationV1 = await controller.createPresentation(); - const presentationV2 = await controller.createPresentationV2(); - - expect(presentationV2.verifiableCredential[0].issuanceDate).not.toBe( - credential.issuanceDate, - ); - expect(presentationV2.verifiableCredential[0].credentialSubject.salary).toBe( - credential.credentialSubject.salary, - ); - - // V2 should match V1 behavior - expect(presentationV2.verifiableCredential[0].issuanceDate).not.toBe( - presentationV1.verifiableCredential[0].issuanceDate, - ); - }); - afterAll(() => closeWallet()); }); diff --git a/packages/core/src/verification-controller.ts b/packages/core/src/verification-controller.ts index 734b9de9..5892e233 100644 --- a/packages/core/src/verification-controller.ts +++ b/packages/core/src/verification-controller.ts @@ -21,10 +21,6 @@ export enum VerificationStatus { SelectingCredentials = 'SelectingCredentials', } -function isRangeProofTemplate(templateJSON) { - return templateJSON.proving_key; -} - type CredentialId = string; type CredentialSelection = { credential: any; @@ -52,7 +48,6 @@ export function createVerificationController({ let filteredCredentials = []; let selectedCredentials: CredentialSelectionMap = new Map(); let selectedDID = null; - let provingKey = null; if (!credentialProvider) { credentialProvider = createCredentialProvider({wallet}); @@ -62,23 +57,6 @@ export function createVerificationController({ didProvider = createDIDProvider({wallet}); } - async function fetchProvingKey(templateJSON: any) { - if (templateJSON.proving_key) { - setState(VerificationStatus.FetchingProvingKey); - try { - provingKey = await axios - .get(templateJSON.proving_key) - .then(res => res.data); - } catch (err) { - setState(VerificationStatus.Error, { - message: 'failed_to_fetch_proving_key', - }); - - throw err; - } - } - } - async function start({template}: {template: string | any}) { setState(VerificationStatus.LoadingTemplate); @@ -96,7 +74,6 @@ export function createVerificationController({ selectedDID = dids[0].didDocument.id; templateJSON = await getJSON(template); - await fetchProvingKey(templateJSON); await loadCredentials(); setState(VerificationStatus.SelectingCredentials); @@ -158,73 +135,6 @@ export function createVerificationController({ assert(!!selectedDID, 'No DID selected'); assert(!!selectedCredentials.size, 'No credentials selected'); - if (isRangeProofTemplate(templateJSON)) { - // TODO: Implement proving key usage for range-proofs - assert(!!provingKey, 'No proving key found'); - } - - const credentials = []; - - for (const credentialSelection of selectedCredentials.values()) { - const isBBS = await isBBSPlusCredential(credentialSelection.credential); - const isKVAC = await isKvacCredential(credentialSelection.credential); - - if (credentialSelection.credential._sd_jwt) { - const derivedCredential = - await credentialServiceRPC.createSDJWTPresentation({ - attributesToReveal: credentialSelection.attributesToReveal, - credential: credentialSelection.credential._sd_jwt.encoded, - }); - - credentials.push(derivedCredential); - } else if (isBBS || isKVAC) { - // derive credential - const derivedCredentials = - await credentialServiceRPC.deriveVCFromPresentation({ - proofRequest: templateJSON, - - credentials: [ - { - credential: credentialSelection.credential, - witness: await credentialProvider.getMembershipWitness(credentialSelection.credential.id), - attributesToReveal: [ - ...(credentialSelection.attributesToReveal || []), - 'id', - ], - }, - ], - }); - - console.log('Credential derived'); - - credentials.push(derivedCredentials[0]); - } else { - credentials.push(credentialSelection.credential); - } - } - - const didKeyPairList = await didProvider.getDIDKeyPairs(); - const keyDoc = didKeyPairList.find(doc => doc.controller === selectedDID); - - assert(keyDoc, `No key pair found for the selected DID ${selectedDID}`); - - const presentation = await credentialServiceRPC.createPresentation({ - credentials, - challenge: templateJSON.nonce, - keyDoc, - id: keyDoc.controller.startsWith('did:key:') - ? keyDoc.id - : `${keyDoc.controller}#keys-1`, - domain: 'dock.io', - }); - - return presentation; - } - - async function createPresentationV2() { - assert(!!selectedDID, 'No DID selected'); - assert(!!selectedCredentials.size, 'No credentials selected'); - const didKeyPairList = await didProvider.getDIDKeyPairs(); const keyDoc = didKeyPairList.find(doc => doc.controller === selectedDID); assert(keyDoc, `No key pair found for the selected DID ${selectedDID}`); @@ -388,7 +298,6 @@ export function createVerificationController({ loadCredentials, getFilteredCredentials, createPresentation, - createPresentationV2, evaluatePresentation, getTemplateJSON() { return templateJSON; From 7d8321b5aa0aed372c59b52915394b9b4e8d61b9 Mon Sep 17 00:00:00 2001 From: Maycon Mello Date: Fri, 6 Mar 2026 15:25:18 -0300 Subject: [PATCH 3/4] Use credential-sdk to generation presentation for pex request --- packages/core/src/verification-controller.ts | 88 ++++--- .../src/services/credential/index.test.js | 218 ++++++++++++++++++ .../wasm/src/services/credential/service.ts | 100 +++++--- 3 files changed, 328 insertions(+), 78 deletions(-) diff --git a/packages/core/src/verification-controller.ts b/packages/core/src/verification-controller.ts index 5892e233..0aa94aaa 100644 --- a/packages/core/src/verification-controller.ts +++ b/packages/core/src/verification-controller.ts @@ -131,6 +131,40 @@ export function createVerificationController({ return credentialServiceRPC.isKvacCredential({credential}); } + async function deriveNonBbsCredentials(sdJwtSelections, regularSelections) { + const credentials = []; + + for (const sel of sdJwtSelections) { + const derived = await credentialServiceRPC.createSDJWTPresentation({ + attributesToReveal: sel.attributesToReveal, + credential: sel.credential._sd_jwt.encoded, + }); + credentials.push(derived); + } + + for (const sel of regularSelections) { + credentials.push(sel.credential); + } + + return credentials; + } + + function getKeyId(keyDoc) { + return keyDoc.controller.startsWith('did:key:') + ? keyDoc.id + : `${keyDoc.controller}#keys-1`; + } + + async function assembleSignedPresentation(credentials, keyDoc) { + return credentialServiceRPC.createPresentation({ + credentials, + challenge: templateJSON.nonce, + keyDoc, + id: getKeyId(keyDoc), + domain: 'dock.io', + }); + } + async function createPresentation() { assert(!!selectedDID, 'No DID selected'); assert(!!selectedCredentials.size, 'No credentials selected'); @@ -166,6 +200,7 @@ export function createVerificationController({ })), ); + // Pure BBS+/KVAC: use generatePresentationFromPex end-to-end if (sdJwtSelections.length === 0 && regularSelections.length === 0) { return credentialServiceRPC.generatePresentationFromPex({ credentials: credentialsWithWitness, @@ -179,6 +214,7 @@ export function createVerificationController({ }); } + // Mixed: derive BBS+/KVAC, then combine with SD-JWT and regular const derivedCredentials = await credentialServiceRPC.deriveVCFromPresentation({ proofRequest: templateJSON, @@ -189,54 +225,16 @@ export function createVerificationController({ })), }); - const allCredentials = [...derivedCredentials]; - - for (const sel of sdJwtSelections) { - const derived = await credentialServiceRPC.createSDJWTPresentation({ - attributesToReveal: sel.attributesToReveal, - credential: sel.credential._sd_jwt.encoded, - }); - allCredentials.push(derived); - } - - for (const sel of regularSelections) { - allCredentials.push(sel.credential); - } - - return credentialServiceRPC.createPresentation({ - credentials: allCredentials, - challenge: templateJSON.nonce, + const nonBbsCredentials = await deriveNonBbsCredentials(sdJwtSelections, regularSelections); + return assembleSignedPresentation( + [...derivedCredentials, ...nonBbsCredentials], keyDoc, - id: keyDoc.controller.startsWith('did:key:') - ? keyDoc.id - : `${keyDoc.controller}#keys-1`, - domain: 'dock.io', - }); - } - - const credentials = []; - - for (const sel of sdJwtSelections) { - const derived = await credentialServiceRPC.createSDJWTPresentation({ - attributesToReveal: sel.attributesToReveal, - credential: sel.credential._sd_jwt.encoded, - }); - credentials.push(derived); - } - - for (const sel of regularSelections) { - credentials.push(sel.credential); + ); } - return credentialServiceRPC.createPresentation({ - credentials, - challenge: templateJSON.nonce, - keyDoc, - id: keyDoc.controller.startsWith('did:key:') - ? keyDoc.id - : `${keyDoc.controller}#keys-1`, - domain: 'dock.io', - }); + // No BBS+/KVAC: handle SD-JWT and regular only + const credentials = await deriveNonBbsCredentials(sdJwtSelections, regularSelections); + return assembleSignedPresentation(credentials, keyDoc); } /** diff --git a/packages/wasm/src/services/credential/index.test.js b/packages/wasm/src/services/credential/index.test.js index 547817bd..0a28bab5 100644 --- a/packages/wasm/src/services/credential/index.test.js +++ b/packages/wasm/src/services/credential/index.test.js @@ -2,6 +2,7 @@ import {assertRpcService, getPromiseError} from '../test-utils'; import {credentialService as service} from './service'; import {validation} from './config'; import * as credentialUtils from '@docknetwork/credential-sdk/vc'; +import * as pexUtils from '@docknetwork/credential-sdk/pex'; import {CredentialServiceRPC} from './service-rpc'; import {OpenID4VCIClientV1_0_13} from '@sphereon/oid4vci-client'; import {didService} from '../dids/service'; @@ -566,4 +567,221 @@ describe('Credential Service', () => { }); }); }); + + describe('generatePresentationFromPex', () => { + let generateSpy; + + const basePexRequest = { + id: 'test-request', + input_descriptors: [{ + id: 'Credential 1', + constraints: {fields: [{path: ['$.type[*]']}]}, + }], + }; + + const baseCredential = { + id: 'https://example.com/credential/1', + type: ['VerifiableCredential'], + credentialSubject: {id: 'did:example:123', name: 'Test'}, + proof: {type: 'Bls12381BBS+SignatureDock2022'}, + }; + + beforeEach(() => { + generateSpy = jest.spyOn(pexUtils, 'generatePresentationFromPexRequest'); + }); + + afterEach(() => { + generateSpy.mockRestore(); + }); + + it('should reject empty credentials array', async () => { + const error = await getPromiseError(() => + service.generatePresentationFromPex({ + credentials: [], + pexRequest: basePexRequest, + challenge: 'test-challenge', + }), + ); + expect(error.message).toBe('no credential found'); + }); + + it('should reject non-array credentials', async () => { + const error = await getPromiseError(() => + service.generatePresentationFromPex({ + credentials: 'invalid', + pexRequest: basePexRequest, + challenge: 'test-challenge', + }), + ); + expect(error.message).toBe('invalid credentials'); + }); + + it('should reject missing pexRequest', async () => { + const error = await getPromiseError(() => + service.generatePresentationFromPex({ + credentials: [{credential: baseCredential}], + challenge: 'test-challenge', + }), + ); + expect(error.message).toBe('pexRequest is required'); + }); + + it('should reject missing challenge', async () => { + const error = await getPromiseError(() => + service.generatePresentationFromPex({ + credentials: [{credential: baseCredential}], + pexRequest: basePexRequest, + }), + ); + expect(error.message).toBe('challenge is required'); + }); + + it('should call generatePresentationFromPexRequest and return presentation', async () => { + const mockPresentation = { + type: ['VerifiablePresentation'], + verifiableCredential: [baseCredential], + }; + + generateSpy.mockResolvedValueOnce({ + status: pexUtils.GeneratePresentationStatus.SUCCESS, + presentation: mockPresentation, + }); + + const result = await service.generatePresentationFromPex({ + credentials: [{credential: baseCredential, attributesToReveal: ['credentialSubject.name']}], + pexRequest: basePexRequest, + challenge: 'test-challenge', + domain: 'dock.io', + skipSigning: true, + }); + + expect(result).toEqual(mockPresentation); + expect(pexUtils.generatePresentationFromPexRequest).toHaveBeenCalledWith( + expect.objectContaining({ + credentials: [baseCredential], + pexRequest: basePexRequest, + challenge: 'test-challenge', + domain: 'dock.io', + skipSigning: true, + selectiveDisclosure: { + credentials: [{ + attributes: ['credentialSubject.name', 'id'], + witness: undefined, + }], + }, + }), + ); + }); + + it('should throw when generation status is not SUCCESS', async () => { + generateSpy.mockResolvedValueOnce({ + status: pexUtils.GeneratePresentationStatus.REQUIREMENTS_NOT_MET, + error: new Error('Credentials do not satisfy the presentation definition'), + }); + + const error = await getPromiseError(() => + service.generatePresentationFromPex({ + credentials: [{credential: baseCredential}], + pexRequest: basePexRequest, + challenge: 'test-challenge', + skipSigning: true, + }), + ); + + expect(error.message).toBe('Credentials do not satisfy the presentation definition'); + }); + + it('should throw generic error when generation fails without error object', async () => { + generateSpy.mockResolvedValueOnce({ + status: pexUtils.GeneratePresentationStatus.REQUIREMENTS_NOT_MET, + }); + + const error = await getPromiseError(() => + service.generatePresentationFromPex({ + credentials: [{credential: baseCredential}], + pexRequest: basePexRequest, + challenge: 'test-challenge', + skipSigning: true, + }), + ); + + expect(error.message).toBe('Presentation generation failed: requirements_not_met'); + }); + + it('should pass credentials without witness as undefined', async () => { + generateSpy.mockResolvedValueOnce({ + status: pexUtils.GeneratePresentationStatus.SUCCESS, + presentation: {type: ['VerifiablePresentation']}, + }); + + await service.generatePresentationFromPex({ + credentials: [{credential: baseCredential, attributesToReveal: []}], + pexRequest: basePexRequest, + challenge: 'test-challenge', + skipSigning: true, + }); + + const callArgs = generateSpy.mock.calls[0][0]; + expect(callArgs.selectiveDisclosure.credentials[0].witness).toBeUndefined(); + }); + + it('should not include loadProvingKey when no boundCheckSnarkKey', async () => { + generateSpy.mockResolvedValueOnce({ + status: pexUtils.GeneratePresentationStatus.SUCCESS, + presentation: {type: ['VerifiablePresentation']}, + }); + + await service.generatePresentationFromPex({ + credentials: [{credential: baseCredential}], + pexRequest: basePexRequest, + challenge: 'test-challenge', + skipSigning: true, + }); + + const callArgs = generateSpy.mock.calls[0][0]; + expect(callArgs.loadProvingKey).toBeUndefined(); + }); + + it('should include loadProvingKey when boundCheckSnarkKey is provided', async () => { + generateSpy.mockResolvedValueOnce({ + status: pexUtils.GeneratePresentationStatus.SUCCESS, + presentation: {type: ['VerifiablePresentation']}, + }); + + await service.generatePresentationFromPex({ + credentials: [{credential: baseCredential}], + pexRequest: basePexRequest, + challenge: 'test-challenge', + boundCheckSnarkKey: 'SGVsbG8gV29ybGQ=', + skipSigning: true, + }); + + const callArgs = generateSpy.mock.calls[0][0]; + expect(typeof callArgs.loadProvingKey).toBe('function'); + }); + + it('should always append id to attributesToReveal', async () => { + generateSpy.mockResolvedValueOnce({ + status: pexUtils.GeneratePresentationStatus.SUCCESS, + presentation: {type: ['VerifiablePresentation']}, + }); + + await service.generatePresentationFromPex({ + credentials: [ + {credential: baseCredential, attributesToReveal: ['credentialSubject.name']}, + {credential: baseCredential}, + ], + pexRequest: basePexRequest, + challenge: 'test-challenge', + skipSigning: true, + }); + + const callArgs = generateSpy.mock.calls[0][0]; + expect(callArgs.selectiveDisclosure.credentials[0].attributes).toEqual([ + 'credentialSubject.name', + 'id', + ]); + expect(callArgs.selectiveDisclosure.credentials[1].attributes).toEqual(['id']); + }); + }); }); diff --git a/packages/wasm/src/services/credential/service.ts b/packages/wasm/src/services/credential/service.ts index 66332ef0..b9363238 100644 --- a/packages/wasm/src/services/credential/service.ts +++ b/packages/wasm/src/services/credential/service.ts @@ -55,6 +55,71 @@ import {isSDJWTCredential as checkIsSDJWT, credentialToW3C as convertCredentialT */ const pex: PEX = new PEX(); +/** + * Resolves the accumulator module class for a credential based on its status ID. + */ +function getAccumulatorModuleClass(credential) { + const statusId = credential?.credentialStatus?.id; + if (!statusId) { + throw new Error('Credential is missing credentialStatus.id required for witness resolution'); + } + const chainModule = + statusId.indexOf('dock:accumulator') === 0 + ? blockchainService.modules.accumulator.modules[0] + : blockchainService.modules.accumulator.modules[ + blockchainService.modules.accumulator.modules.length - 1 + ]; + return chainModule.constructor; +} + +/** + * Resolves the witness for a single credential into the format expected by credential-sdk. + * Returns undefined if the credential has no witness. + */ +async function resolveWitnessForCredential(credential, witness) { + if (!witness) { + return undefined; + } + + try { + const details = await getWitnessDetails(credential, witness); + const accumulatorModuleClass = getAccumulatorModuleClass(credential); + return { + membershipWitness: details.membershipWitness, + accumulated: accumulatorModuleClass.accumulatedFromHex( + details.accumulator.accumulated, + AccumulatorType.VBPos, + ), + pk: details.pk, + params: details.params, + }; + } catch (err) { + throw new Error( + `Failed to resolve witness for credential ${credential?.id || 'unknown'}: ${err.message}`, + ); + } +} + +/** + * Creates a loadProvingKey callback for bound check proofs. + * Returns undefined if no boundCheckSnarkKey is provided. + */ +function createProvingKeyLoader(boundCheckSnarkKey) { + if (!boundCheckSnarkKey) { + return undefined; + } + + return async () => { + const blob = (await isBase64OrDataUrl(boundCheckSnarkKey)) + ? blobFromBase64(boundCheckSnarkKey) + : await fetchBlobFromUrl(boundCheckSnarkKey); + return { + provingKey: new LegoProvingKey(blob), + provingKeyId: 'key0', + }; + }; +} + /** * Checks if a credential uses BBS+ signature * @param {Object} credential - The credential to check @@ -762,28 +827,7 @@ class CredentialService { } = params; const resolvedWitnesses = await Promise.all( - credentials.map(async (c) => { - if (!c.witness) { - return undefined; - } - const details = await getWitnessDetails(c.credential, c.witness); - const chainModule = - c.credential.credentialStatus.id.indexOf('dock:accumulator') === 0 - ? blockchainService.modules.accumulator.modules[0] - : blockchainService.modules.accumulator.modules[ - blockchainService.modules.accumulator.modules.length - 1 - ]; - const accumulatorModuleClass = chainModule.constructor; - return { - membershipWitness: details.membershipWitness, - accumulated: accumulatorModuleClass.accumulatedFromHex( - details.accumulator.accumulated, - AccumulatorType.VBPos, - ), - pk: details.pk, - params: details.params, - }; - }), + credentials.map(c => resolveWitnessForCredential(c.credential, c.witness)), ); const result = await generatePresentationFromPexRequest({ @@ -795,17 +839,7 @@ class CredentialService { domain, resolver: blockchainService.resolver, skipSigning: skipSigning || false, - loadProvingKey: boundCheckSnarkKey - ? async () => { - const blob = (await isBase64OrDataUrl(boundCheckSnarkKey)) - ? blobFromBase64(boundCheckSnarkKey) - : await fetchBlobFromUrl(boundCheckSnarkKey); - return { - provingKey: new LegoProvingKey(blob), - provingKeyId: 'key0', - }; - } - : undefined, + loadProvingKey: createProvingKeyLoader(boundCheckSnarkKey), selectiveDisclosure: { credentials: credentials.map((c, i) => ({ attributes: [...(c.attributesToReveal || []), 'id'], From b46e85d7cdc1a846a0f14987e493aa2435d33aa7 Mon Sep 17 00:00:00 2001 From: Maycon Mello Date: Fri, 6 Mar 2026 15:36:17 -0300 Subject: [PATCH 4/4] Fix prettier formatting in generatePresentationFromPex tests --- .../src/services/credential/index.test.js | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/wasm/src/services/credential/index.test.js b/packages/wasm/src/services/credential/index.test.js index 0a28bab5..e0037cca 100644 --- a/packages/wasm/src/services/credential/index.test.js +++ b/packages/wasm/src/services/credential/index.test.js @@ -573,10 +573,12 @@ describe('Credential Service', () => { const basePexRequest = { id: 'test-request', - input_descriptors: [{ - id: 'Credential 1', - constraints: {fields: [{path: ['$.type[*]']}]}, - }], + input_descriptors: [ + { + id: 'Credential 1', + constraints: {fields: [{path: ['$.type[*]']}]}, + }, + ], }; const baseCredential = { @@ -648,7 +650,12 @@ describe('Credential Service', () => { }); const result = await service.generatePresentationFromPex({ - credentials: [{credential: baseCredential, attributesToReveal: ['credentialSubject.name']}], + credentials: [ + { + credential: baseCredential, + attributesToReveal: ['credentialSubject.name'], + }, + ], pexRequest: basePexRequest, challenge: 'test-challenge', domain: 'dock.io', @@ -664,10 +671,12 @@ describe('Credential Service', () => { domain: 'dock.io', skipSigning: true, selectiveDisclosure: { - credentials: [{ - attributes: ['credentialSubject.name', 'id'], - witness: undefined, - }], + credentials: [ + { + attributes: ['credentialSubject.name', 'id'], + witness: undefined, + }, + ], }, }), ); @@ -676,7 +685,9 @@ describe('Credential Service', () => { it('should throw when generation status is not SUCCESS', async () => { generateSpy.mockResolvedValueOnce({ status: pexUtils.GeneratePresentationStatus.REQUIREMENTS_NOT_MET, - error: new Error('Credentials do not satisfy the presentation definition'), + error: new Error( + 'Credentials do not satisfy the presentation definition', + ), }); const error = await getPromiseError(() => @@ -688,7 +699,9 @@ describe('Credential Service', () => { }), ); - expect(error.message).toBe('Credentials do not satisfy the presentation definition'); + expect(error.message).toBe( + 'Credentials do not satisfy the presentation definition', + ); }); it('should throw generic error when generation fails without error object', async () => { @@ -705,7 +718,9 @@ describe('Credential Service', () => { }), ); - expect(error.message).toBe('Presentation generation failed: requirements_not_met'); + expect(error.message).toBe( + 'Presentation generation failed: requirements_not_met', + ); }); it('should pass credentials without witness as undefined', async () => { @@ -722,7 +737,9 @@ describe('Credential Service', () => { }); const callArgs = generateSpy.mock.calls[0][0]; - expect(callArgs.selectiveDisclosure.credentials[0].witness).toBeUndefined(); + expect( + callArgs.selectiveDisclosure.credentials[0].witness, + ).toBeUndefined(); }); it('should not include loadProvingKey when no boundCheckSnarkKey', async () => { @@ -768,7 +785,10 @@ describe('Credential Service', () => { await service.generatePresentationFromPex({ credentials: [ - {credential: baseCredential, attributesToReveal: ['credentialSubject.name']}, + { + credential: baseCredential, + attributesToReveal: ['credentialSubject.name'], + }, {credential: baseCredential}, ], pexRequest: basePexRequest, @@ -781,7 +801,9 @@ describe('Credential Service', () => { 'credentialSubject.name', 'id', ]); - expect(callArgs.selectiveDisclosure.credentials[1].attributes).toEqual(['id']); + expect(callArgs.selectiveDisclosure.credentials[1].attributes).toEqual([ + 'id', + ]); }); }); });