From 7876a0cabee96ce433e6cd2e4348f8ace4607329 Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 14 Oct 2025 13:17:15 -0300 Subject: [PATCH 1/9] DCKA-5152: support for SD-jwt --- package-lock.json | 234 ++++++++ packages/core/src/credential-provider.ts | 15 + packages/core/src/verification-controller.ts | 10 +- packages/wasm/package.json | 3 + .../src/services/credential/sd-jwt.test.ts | 499 ++++++++++++++++++ .../wasm/src/services/credential/sd-jwt.ts | 238 +++++++++ .../wasm/src/services/credential/service.ts | 61 ++- 7 files changed, 1058 insertions(+), 2 deletions(-) create mode 100644 packages/wasm/src/services/credential/sd-jwt.test.ts create mode 100644 packages/wasm/src/services/credential/sd-jwt.ts diff --git a/package-lock.json b/package-lock.json index 9750d50f..e6fee181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6159,6 +6159,60 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@sd-jwt/core": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/core/-/core-0.15.0.tgz", + "integrity": "sha512-H+I8OcZj8/f6GZxNSblJqJXi6Mo/W5sUcVNiW9K6kc0rEGt+na111zS1vpjbG6QEJT/YC9egIVUHXNJlNIlWxw==", + "dependencies": { + "@sd-jwt/decode": "0.15.0", + "@sd-jwt/present": "0.15.0", + "@sd-jwt/types": "0.15.0", + "@sd-jwt/utils": "0.15.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/core/node_modules/@sd-jwt/decode": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/decode/-/decode-0.15.0.tgz", + "integrity": "sha512-c9PX4/Cnl1G0pSUXU236lkDx1AzkbgGKFnevBaVIPX8WlJl0nII345uDW/KWCV4rs5Ci8j4ZviWVOJm8TCt2fg==", + "dependencies": { + "@sd-jwt/types": "0.15.0", + "@sd-jwt/utils": "0.15.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/core/node_modules/@sd-jwt/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.15.0.tgz", + "integrity": "sha512-3b/jz4xkQ9HAOVtnR5s+R9qzqPUig8EvTK4kDOL1rSdTHSQ8E50+Gji1k2m/0hF/DP4EskWc5iQJKrk2qRohaQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/core/node_modules/@sd-jwt/utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/utils/-/utils-0.15.0.tgz", + "integrity": "sha512-eHenn93LQD1DirBwz8bgE5vEH+sJaKKP+1y9kk2OGuPh1vLiw1hT0O7zlvRVHf9Z9SFYe09ZVucH0jDkjo5OAw==", + "dependencies": { + "@sd-jwt/types": "0.15.0", + "js-base64": "^3.7.6" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/crypto-nodejs": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/crypto-nodejs/-/crypto-nodejs-0.15.0.tgz", + "integrity": "sha512-qWoXeJL/knLUDYJ5s3elOwT5iakGz3gGpjdM3F9Xghq0lx6MdOUEBdLoJuGzhkyxCVg2eBNewLqsaeg7h2Ik/Q==", + "engines": { + "node": ">=18" + } + }, "node_modules/@sd-jwt/decode": { "version": "0.6.1", "license": "Apache-2.0", @@ -6170,6 +6224,132 @@ "node": ">=16" } }, + "node_modules/@sd-jwt/jwt-status-list": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/jwt-status-list/-/jwt-status-list-0.15.0.tgz", + "integrity": "sha512-+wy0+Z5tgO6hjTSjrDV9o/fjxIBqWajsTl3cU//u2mZpPFWljLCvZgC0743iwphkkRf9hSQhRsPIgH5mIWtRVQ==", + "dependencies": { + "@sd-jwt/types": "0.15.0", + "base64url": "^3.0.1", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/jwt-status-list/node_modules/@sd-jwt/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.15.0.tgz", + "integrity": "sha512-3b/jz4xkQ9HAOVtnR5s+R9qzqPUig8EvTK4kDOL1rSdTHSQ8E50+Gji1k2m/0hF/DP4EskWc5iQJKrk2qRohaQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/jwt-status-list/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "node_modules/@sd-jwt/present": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/present/-/present-0.15.0.tgz", + "integrity": "sha512-Wf/KUKDe06QjnRBzaN6fMAtYPVynQZ+hJOSnpDiNU3E2k3X1KB/rPy7J4uT08ch3ZwJN+08FR7aFsrKvWjjsmg==", + "dependencies": { + "@sd-jwt/decode": "0.15.0", + "@sd-jwt/types": "0.15.0", + "@sd-jwt/utils": "0.15.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/present/node_modules/@sd-jwt/decode": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/decode/-/decode-0.15.0.tgz", + "integrity": "sha512-c9PX4/Cnl1G0pSUXU236lkDx1AzkbgGKFnevBaVIPX8WlJl0nII345uDW/KWCV4rs5Ci8j4ZviWVOJm8TCt2fg==", + "dependencies": { + "@sd-jwt/types": "0.15.0", + "@sd-jwt/utils": "0.15.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/present/node_modules/@sd-jwt/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.15.0.tgz", + "integrity": "sha512-3b/jz4xkQ9HAOVtnR5s+R9qzqPUig8EvTK4kDOL1rSdTHSQ8E50+Gji1k2m/0hF/DP4EskWc5iQJKrk2qRohaQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/present/node_modules/@sd-jwt/utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/utils/-/utils-0.15.0.tgz", + "integrity": "sha512-eHenn93LQD1DirBwz8bgE5vEH+sJaKKP+1y9kk2OGuPh1vLiw1hT0O7zlvRVHf9Z9SFYe09ZVucH0jDkjo5OAw==", + "dependencies": { + "@sd-jwt/types": "0.15.0", + "js-base64": "^3.7.6" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@sd-jwt/sd-jwt-vc/-/sd-jwt-vc-0.15.1.tgz", + "integrity": "sha512-xgs611LuHwNU71sw1dcWluFB0C+20uqvsuQdpcPnLckKu0yyxmm9Sb2s01Ec0uBvC3HGdQAgmP/I/B0mHXYPUA==", + "dependencies": { + "@sd-jwt/core": "0.15.0", + "@sd-jwt/jwt-status-list": "0.15.0", + "@sd-jwt/utils": "0.15.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/@sd-jwt/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.15.0.tgz", + "integrity": "sha512-3b/jz4xkQ9HAOVtnR5s+R9qzqPUig8EvTK4kDOL1rSdTHSQ8E50+Gji1k2m/0hF/DP4EskWc5iQJKrk2qRohaQ==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/@sd-jwt/utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@sd-jwt/utils/-/utils-0.15.0.tgz", + "integrity": "sha512-eHenn93LQD1DirBwz8bgE5vEH+sJaKKP+1y9kk2OGuPh1vLiw1hT0O7zlvRVHf9Z9SFYe09ZVucH0jDkjo5OAw==", + "dependencies": { + "@sd-jwt/types": "0.15.0", + "js-base64": "^3.7.6" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/@sd-jwt/types": { "version": "0.6.1", "license": "Apache-2.0", @@ -8631,6 +8811,42 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "license": "MIT", @@ -15291,6 +15507,21 @@ "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, "node_modules/fastq": { "version": "1.15.0", "license": "ISC", @@ -32323,11 +32554,14 @@ "@docknetwork/wallet-sdk-dids": "^1.5.14", "@noble/hashes": "1.8.0", "@scure/bip39": "^1.6.0", + "@sd-jwt/crypto-nodejs": "^0.15.0", + "@sd-jwt/sd-jwt-vc": "^0.15.1", "@sphereon/oid4vci-client": "^0.16.0", "@sphereon/oid4vci-common": "^0.16.0", "@sphereon/pex": "^1.1.4", "axios": "^0.25.0", "axios-retry": "^3.2.5", + "base64url": "^3.0.1", "cwait": "1.1.2", "json-rpc-2.0": "^0.2.16", "p-limit": "2.3.0", diff --git a/packages/core/src/credential-provider.ts b/packages/core/src/credential-provider.ts index c2b45a63..a29ea842 100644 --- a/packages/core/src/credential-provider.ts +++ b/packages/core/src/credential-provider.ts @@ -173,6 +173,21 @@ export const ACUMM_WITNESS_PROP_KEY = '$$accum__witness$$'; * @private */ export async function addCredential({wallet, credential}) { + // Check if the credential is an SD-JWT (string format) + if (typeof credential === 'string') { + try { + const isSDJWT = await credentialServiceRPC.isSDJWTCredential({credential}); + + if (isSDJWT) { + // Convert SD-JWT to W3C format (includes _sd_jwt metadata for unwrapping) + credential = await credentialServiceRPC.credentialToW3C({credential}); + } + } catch (error) { + console.error('Error checking/converting SD-JWT credential:', error); + throw new Error('Failed to process SD-JWT credential: ' + error.message); + } + } + const acummWitness = credential[ACUMM_WITNESS_PROP_KEY]; if (acummWitness) { diff --git a/packages/core/src/verification-controller.ts b/packages/core/src/verification-controller.ts index a1cb13d3..3ec9033d 100644 --- a/packages/core/src/verification-controller.ts +++ b/packages/core/src/verification-controller.ts @@ -169,7 +169,15 @@ export function createVerificationController({ const isBBS = await isBBSPlusCredential(credentialSelection.credential); const isKVAC = await isKvacCredential(credentialSelection.credential); - if (isBBS || isKVAC) { + 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({ diff --git a/packages/wasm/package.json b/packages/wasm/package.json index b06299f7..0897125f 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -30,6 +30,9 @@ "@docknetwork/credential-sdk": "0.54.3", "@sphereon/oid4vci-client": "^0.16.0", "@sphereon/oid4vci-common": "^0.16.0", + "@sd-jwt/crypto-nodejs": "^0.15.0", + "@sd-jwt/sd-jwt-vc": "^0.15.1", + "base64url": "^3.0.1", "axios": "^0.25.0", "json-rpc-2.0": "^0.2.16", "uuid": "^8.3.2", diff --git a/packages/wasm/src/services/credential/sd-jwt.test.ts b/packages/wasm/src/services/credential/sd-jwt.test.ts new file mode 100644 index 00000000..46793823 --- /dev/null +++ b/packages/wasm/src/services/credential/sd-jwt.test.ts @@ -0,0 +1,499 @@ +import { + isSDJWTCredential, + sdJwtToW3C, + decodeSDJWTToW3C, + credentialToW3C, + verifySDJWT, + decodeSDJWT, + createSDJWTPresentation, +} from './sd-jwt'; + +// Test SD-JWT credential provided by the user +const TEST_SD_JWT = 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJraWQiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDpjMDg5MGYxYy1jN2JiLTRlYTYtYmU3YS04YzMxNDA0NzQzYjcja2V5cy0xIiwiYWxnIjoiRWREU0EifQ.eyJpYXQiOjE3NTk0MTQzOTQsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OmMwODkwZjFjLWM3YmItNGVhNi1iZTdhLThjMzE0MDQ3NDNiNyNrZXlzLTEiLCJ2Y3QiOiJJbnRlcm5hbFRlc3RpbmciLCJfc2QiOlsiM3JVUGt1Mk5XckFFeTV3ZE9uVms5TkJBa0haMWE4RDB6Y2liMFNQdmthWSIsIkhjNHAxMnZTTWNMQ0piNVRYVlFrdFFMR2xjM0J3SDNWN2ltakV5ZDdvdzAiLCJNZWNNZEd6NjAxY3kwTTdvanRtSjR1LUI5LTJxSXAya1RvbFpDUm1GZ1pFIiwiZmo4WHdBb0lERmRsWmpEa0NTVzVpeXBPYUZBcVplTWZDRncwTWd3cHhOQSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.FcYvNrldceL5BTNmoIaS4Mub8a5NcbiseUeSmmvUpOW8SUom-bchV5AEefrH1VMECbdc2whhk2sW4_jmZo7_Dw~WyI1YTRkZWY1MTAzYjRiYjc0IiwiaWQiLCJkaWQ6a2V5Ono2TWt1OVI4emRBOExENmhjRlhrbjQ3akxuZmNLWk5HbXdhVHJEbmFDQmtTYjhVbiJd~WyJiZDUyMjQ5ZjAyNjk3NWRkIiwiZGF0ZSIsIjIwMjUtMTAtMDkiXQ~WyJlMjcxNzExNzVkODdlMWE1IiwibmFtZSIsIm1heWNvbiJd~WyIwZDFiOTBlNTlhNmMyNDNiIiwibnVtYmVyIiwxMjNd~'; + +// Regular JWT (not SD-JWT) for testing +const REGULAR_JWT = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + +// Regular JWT with dc+sd-jwt type +const DC_SD_JWT = 'eyJ0eXAiOiJkYytzZC1qd3QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + +// Sample W3C credential for testing +const W3C_CREDENTIAL = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential', 'TestCredential'], + issuer: 'did:example:issuer', + credentialSubject: { + id: 'did:example:subject', + name: 'Test Subject', + }, +}; + +describe('SD-JWT Service', () => { + describe('isSDJWTCredential', () => { + it('should return true for vc+sd-jwt credential', () => { + const result = isSDJWTCredential(TEST_SD_JWT); + expect(result).toBe(true); + }); + + it('should return true for dc+sd-jwt credential', () => { + const result = isSDJWTCredential(DC_SD_JWT); + expect(result).toBe(true); + }); + + it('should return false for regular JWT', () => { + const result = isSDJWTCredential(REGULAR_JWT); + expect(result).toBe(false); + }); + + it('should handle JWT without typ field', () => { + const jwtWithoutTyp = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U'; + const result = isSDJWTCredential(jwtWithoutTyp); + expect(result).toBe(false); + }); + }); + + describe('sdJwtToW3C', () => { + it('should convert decoded SD-JWT to W3C format with all fields', () => { + const decodedSDJWT = { + jwt: { + header: { + typ: 'vc+sd-jwt', + alg: 'EdDSA', + }, + payload: { + iss: 'did:example:issuer', + vct: 'TestCredential', + iat: 1609459200, // 2021-01-01T00:00:00.000Z + exp: 1640995200, // 2022-01-01T00:00:00.000Z + jti: 'credential-id-123', + }, + }, + disclosures: [ + { key: 'id', value: 'did:example:subject' }, + { key: 'name', value: 'John Doe' }, + { key: 'age', value: 30 }, + ], + }; + + const result = sdJwtToW3C(decodedSDJWT); + + expect(result).toEqual({ + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential', 'TestCredential'], + issuer: 'did:example:issuer', + credentialSubject: { + id: 'did:example:subject', + name: 'John Doe', + age: 30, + }, + issuanceDate: '2021-01-01T00:00:00.000Z', + expirationDate: '2022-01-01T00:00:00.000Z', + id: 'credential-id-123', + _sd_jwt: { + encoded: undefined, + decoded: decodedSDJWT, + }, + }); + }); + + it('should store raw encoded SD-JWT when provided', () => { + const decodedSDJWT = { + jwt: { + header: { + typ: 'vc+sd-jwt', + alg: 'EdDSA', + }, + payload: { + iss: 'did:example:issuer', + vct: 'TestCredential', + }, + }, + disclosures: [ + { key: 'name', value: 'Jane Doe' }, + ], + }; + + const encodedSDJWT = 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIifQ.signature~disclosure1~disclosure2~'; + const result = sdJwtToW3C(decodedSDJWT, encodedSDJWT); + + expect(result._sd_jwt).toBeDefined(); + expect(result._sd_jwt.encoded).toBe(encodedSDJWT); + expect(result._sd_jwt.decoded).toEqual(decodedSDJWT); + }); + + it('should handle minimal SD-JWT without optional fields', () => { + const decodedSDJWT = { + jwt: { + header: { + typ: 'vc+sd-jwt', + alg: 'EdDSA', + }, + payload: { + iss: 'did:example:issuer', + vct: 'TestCredential', + }, + }, + disclosures: [ + { key: 'name', value: 'Jane Doe' }, + ], + }; + + const result = sdJwtToW3C(decodedSDJWT); + + expect(result).toEqual({ + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential', 'TestCredential'], + issuer: 'did:example:issuer', + credentialSubject: { + name: 'Jane Doe', + }, + _sd_jwt: { + encoded: undefined, + decoded: decodedSDJWT, + }, + }); + }); + + it('should handle empty disclosures array', () => { + const decodedSDJWT = { + jwt: { + header: { + typ: 'vc+sd-jwt', + alg: 'EdDSA', + }, + payload: { + iss: 'did:example:issuer', + vct: 'TestCredential', + }, + }, + disclosures: [], + }; + + const result = sdJwtToW3C(decodedSDJWT); + + expect(result.credentialSubject).toEqual({}); + }); + + it('should use issuer field from payload if iss is not present', () => { + const decodedSDJWT = { + jwt: { + header: { + typ: 'vc+sd-jwt', + alg: 'EdDSA', + }, + payload: { + issuer: 'did:example:issuer-from-issuer-field', + vct: 'TestCredential', + }, + }, + disclosures: [], + }; + + const result = sdJwtToW3C(decodedSDJWT); + + expect(result.issuer).toBe('did:example:issuer-from-issuer-field'); + }); + + it('should handle disclosures without key (malformed)', () => { + const decodedSDJWT = { + jwt: { + header: { + typ: 'vc+sd-jwt', + alg: 'EdDSA', + }, + payload: { + iss: 'did:example:issuer', + vct: 'TestCredential', + }, + }, + disclosures: [ + { key: 'name', value: 'Valid' }, + { value: 'Invalid' }, // Missing key + null, // Null disclosure + ], + }; + + const result = sdJwtToW3C(decodedSDJWT); + + expect(result.credentialSubject).toEqual({ + name: 'Valid', + }); + }); + + it('should use UnknownCredential when vct is missing', () => { + const decodedSDJWT = { + jwt: { + header: { + typ: 'vc+sd-jwt', + alg: 'EdDSA', + }, + payload: { + iss: 'did:example:issuer', + }, + }, + disclosures: [], + }; + + const result = sdJwtToW3C(decodedSDJWT); + + expect(result.type).toEqual(['VerifiableCredential', 'UnknownCredential']); + }); + }); + + describe('decodeSDJWTToW3C', () => { + it('should decode the provided test SD-JWT and convert to W3C format', async () => { + const result = await decodeSDJWTToW3C(TEST_SD_JWT) as any; + + // Verify the structure + expect(result).toHaveProperty('@context'); + expect(result).toHaveProperty('type'); + expect(result).toHaveProperty('issuer'); + expect(result).toHaveProperty('credentialSubject'); + expect(result).toHaveProperty('issuanceDate'); + + // Verify specific values from the test SD-JWT + expect(result.type).toEqual(['VerifiableCredential', 'InternalTesting']); + expect(result.issuer).toBe('did:cheqd:testnet:c0890f1c-c7bb-4ea6-be7a-8c31404743b7#keys-1'); + + // Verify credential subject has disclosed claims + expect(result.credentialSubject).toHaveProperty('id'); + expect(result.credentialSubject).toHaveProperty('name'); + expect(result.credentialSubject).toHaveProperty('date'); + expect(result.credentialSubject).toHaveProperty('number'); + + // Verify disclosed values + expect(result.credentialSubject.id).toBe('did:key:z6Mku9R8zdA8LD6hcFXkn47jLnfcKZNGmwaTrDnaCBkSb8Un'); + expect(result.credentialSubject.name).toBe('maycon'); + expect(result.credentialSubject.date).toBe('2025-10-09'); + expect(result.credentialSubject.number).toBe(123); + + // Verify issuance date + expect(result.issuanceDate).toBe(new Date(1759414394 * 1000).toISOString()); + }); + + it('should store raw encoded SD-JWT and decoded structure in metadata', async () => { + const result = await decodeSDJWTToW3C(TEST_SD_JWT) as any; + + // Verify _sd_jwt metadata is present + expect(result).toHaveProperty('_sd_jwt'); + expect(result._sd_jwt).toHaveProperty('encoded'); + expect(result._sd_jwt).toHaveProperty('decoded'); + + // Verify raw SD-JWT string is stored + expect(result._sd_jwt.encoded).toBe(TEST_SD_JWT); + + // Verify decoded structure contains expected fields + expect(result._sd_jwt.decoded).toHaveProperty('jwt'); + expect(result._sd_jwt.decoded).toHaveProperty('disclosures'); + expect(result._sd_jwt.decoded.jwt).toHaveProperty('header'); + expect(result._sd_jwt.decoded.jwt).toHaveProperty('payload'); + + // Verify we can access disclosure information + expect(Array.isArray(result._sd_jwt.decoded.disclosures)).toBe(true); + expect(result._sd_jwt.decoded.disclosures.length).toBeGreaterThan(0); + }); + + it('should handle SD-JWT with minimal disclosures', async () => { + // This test would need a valid minimal SD-JWT + // For now, we'll just test that the function is async and can be called + await expect(decodeSDJWTToW3C(TEST_SD_JWT)).resolves.toBeDefined(); + }); + }); + + describe('credentialToW3C', () => { + it('should return W3C credential as-is when passed an object with type', async () => { + const result = await credentialToW3C(W3C_CREDENTIAL); + expect(result).toEqual(W3C_CREDENTIAL); + }); + + it('should decode SD-JWT string to W3C format', async () => { + const result = await credentialToW3C(TEST_SD_JWT); + + expect(result).toHaveProperty('@context'); + expect(result).toHaveProperty('type'); + expect(result).toHaveProperty('issuer'); + expect(result).toHaveProperty('credentialSubject'); + expect(result.type).toEqual(['VerifiableCredential', 'InternalTesting']); + }); + + it('should parse JSON string containing W3C credential', async () => { + const jsonString = JSON.stringify(W3C_CREDENTIAL); + const result = await credentialToW3C(jsonString); + expect(result).toEqual(W3C_CREDENTIAL); + }); + + it('should handle credential object with data field', async () => { + const credentialWrapper = { + data: W3C_CREDENTIAL, + }; + const result = await credentialToW3C(credentialWrapper); + expect(result).toEqual(W3C_CREDENTIAL); + }); + + it('should handle nested credential object with SD-JWT in data field', async () => { + const credentialWrapper = { + data: TEST_SD_JWT, + }; + const result = await credentialToW3C(credentialWrapper); + + expect(result).toHaveProperty('@context'); + expect(result).toHaveProperty('type'); + expect(result.type).toEqual(['VerifiableCredential', 'InternalTesting']); + }); + + it('should throw error for unsupported credential format', async () => { + const invalidCredential = 'not-a-valid-jwt-or-credential'; + + await expect(credentialToW3C(invalidCredential)).rejects.toThrow( + 'Unable to convert credential to W3C format' + ); + }); + + it('should throw error for empty string', async () => { + await expect(credentialToW3C('')).rejects.toThrow(); + }); + + it('should handle object without type or data field', async () => { + const invalidObject = { + someField: 'someValue', + }; + + await expect(credentialToW3C(invalidObject)).rejects.toThrow( + 'Unable to convert credential to W3C format' + ); + }); + + it('should parse JSON string containing credential with data field', async () => { + const jsonString = JSON.stringify({ + data: W3C_CREDENTIAL, + }); + const parsedObject = JSON.parse(jsonString); + const result = await credentialToW3C(parsedObject); + expect(result).toEqual(W3C_CREDENTIAL); + }); + + it('should handle regular JWT (not SD-JWT)', async () => { + await expect(credentialToW3C(REGULAR_JWT)).rejects.toThrow( + 'Unable to convert credential to W3C format' + ); + }); + }); + + describe('Integration tests', () => { + it('should properly decode and convert complete SD-JWT flow', async () => { + // Test the complete flow: check if SD-JWT -> decode -> convert to W3C + const isSDJWT = isSDJWTCredential(TEST_SD_JWT); + expect(isSDJWT).toBe(true); + + const w3cCredential = await decodeSDJWTToW3C(TEST_SD_JWT) as any; + + // Verify complete structure + expect(w3cCredential).toMatchObject({ + '@context': expect.arrayContaining(['https://www.w3.org/2018/credentials/v1']), + type: expect.arrayContaining(['VerifiableCredential', 'InternalTesting']), + issuer: 'did:cheqd:testnet:c0890f1c-c7bb-4ea6-be7a-8c31404743b7#keys-1', + credentialSubject: { + id: 'did:key:z6Mku9R8zdA8LD6hcFXkn47jLnfcKZNGmwaTrDnaCBkSb8Un', + name: 'maycon', + date: '2025-10-09', + number: 123, + }, + }); + + // Verify SD-JWT metadata is stored for unwrapping + expect(w3cCredential._sd_jwt).toBeDefined(); + expect(w3cCredential._sd_jwt.encoded).toBe(TEST_SD_JWT); + expect(w3cCredential._sd_jwt.decoded).toBeDefined(); + }); + + it('should handle credentialToW3C with the test SD-JWT', async () => { + const result = await credentialToW3C(TEST_SD_JWT) as any; + + expect(result.credentialSubject.name).toBe('maycon'); + expect(result.credentialSubject.number).toBe(123); + expect(result.issuer).toBe('did:cheqd:testnet:c0890f1c-c7bb-4ea6-be7a-8c31404743b7#keys-1'); + + // Verify SD-JWT metadata for presentation unwrapping + expect(result._sd_jwt).toBeDefined(); + expect(result._sd_jwt.encoded).toBe(TEST_SD_JWT); + expect(result._sd_jwt.decoded).toBeDefined(); + }); + }); + + describe('decodeSDJWT', () => { + it('should decode SD-JWT into structured format', async () => { + const decoded = await decodeSDJWT(TEST_SD_JWT); + + expect(decoded).toHaveProperty('jwt'); + expect(decoded).toHaveProperty('disclosures'); + expect(decoded.jwt).toHaveProperty('header'); + expect(decoded.jwt).toHaveProperty('payload'); + expect(Array.isArray(decoded.disclosures)).toBe(true); + }); + + it('should decode header and payload correctly', async () => { + const decoded = await decodeSDJWT(TEST_SD_JWT); + + expect(decoded.jwt.header.typ).toBe('vc+sd-jwt'); + expect(decoded.jwt.header.alg).toBe('EdDSA'); + expect(decoded.jwt.payload.iss).toBe('did:cheqd:testnet:c0890f1c-c7bb-4ea6-be7a-8c31404743b7#keys-1'); + expect(decoded.jwt.payload.vct).toBe('InternalTesting'); + }); + + it('should throw error for invalid SD-JWT', async () => { + await expect(decodeSDJWT('invalid-jwt-string')).rejects.toThrow(); + }); + }); + + describe('verifySDJWT', () => { + it('should verify valid SD-JWT successfully', async () => { + const result = await verifySDJWT(TEST_SD_JWT); + + expect(result).toHaveProperty('verified'); + expect(result.verified).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it('should fail verification for expired SD-JWT', async () => { + // Create an expired SD-JWT (exp in the past) + const expiredJWT = 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIiLCJ2Y3QiOiJUZXN0Q3JlZGVudGlhbCIsImV4cCI6MTYwOTQ1OTIwMCwiaWF0IjoxNjA5NDU5MjAwfQ.signature~'; + + const result = await verifySDJWT(expiredJWT); + + expect(result.verified).toBe(false); + expect(result.error).toContain('expired'); + }); + + it('should fail verification for not-yet-valid SD-JWT', async () => { + // Create an SD-JWT with nbf in the future + const futureJWT = 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIiLCJ2Y3QiOiJUZXN0Q3JlZGVudGlhbCIsIm5iZiI6OTk5OTk5OTk5OSwiaWF0IjoxNjA5NDU5MjAwfQ.signature~'; + + const result = await verifySDJWT(futureJWT); + + expect(result.verified).toBe(false); + expect(result.error).toContain('not yet valid'); + }); + + it('should fail verification for invalid SD-JWT format', async () => { + const result = await verifySDJWT('invalid-jwt-string'); + + expect(result.verified).toBe(false); + expect(result.error).toBeDefined(); + }); + + it('should verify SD-JWT without expiration date', async () => { + // The TEST_SD_JWT doesn't have an expiration date, so it should verify + const result = await verifySDJWT(TEST_SD_JWT); + + expect(result.verified).toBe(true); + }); + + it('should return error message on verification failure', async () => { + const result = await verifySDJWT(''); + + expect(result.verified).toBe(false); + expect(result).toHaveProperty('error'); + expect(typeof result.error).toBe('string'); + }); + }); +}); diff --git a/packages/wasm/src/services/credential/sd-jwt.ts b/packages/wasm/src/services/credential/sd-jwt.ts new file mode 100644 index 00000000..2ef2fc44 --- /dev/null +++ b/packages/wasm/src/services/credential/sd-jwt.ts @@ -0,0 +1,238 @@ +import {SDJwtVcInstance} from '@sd-jwt/sd-jwt-vc'; +import {digest, generateSalt} from '@sd-jwt/crypto-nodejs'; +import base64url from 'base64url'; + +/** + * Checks if a JWT string is an SD-JWT credential + */ +export function isSDJWTCredential(jwt) { + const jwtHeader = jwt.split('.')[0]; + const decodedHeader = JSON.parse(base64url.decode(jwtHeader)); + return decodedHeader.typ === 'dc+sd-jwt' || decodedHeader.typ === 'vc+sd-jwt'; +} + +export async function createSDJWTPresentation({ + attributesToReveal, + credential, +}: { + attributesToReveal: string[]; + credential: string; +}) { + const sdjwt = new SDJwtVcInstance({ + signAlg: 'EdDSA', + hasher: digest, + hashAlg: 'sha-256', + saltGenerator: generateSalt, + }); + + // Holder defines the presentation frame to specify which claims should be presented + // The list of presented claims must be a subset of the disclosed claims + const presentationFrame: any = {}; + attributesToReveal.forEach(attribute => { + presentationFrame[attribute] = true; + }); + + // Holder creates a presentation using the issued credential and the presentation frame + // returns an encoded SD JWT. + const presentation = await sdjwt.present(credential, presentationFrame); + + return presentation; +} +/** + * Decodes an SD-JWT string into its structured format + * @param {string} sdJwtString - The SD-JWT string to decode + * @returns {Promise} Decoded SD-JWT structure with jwt and disclosures + */ +export async function decodeSDJWT(sdJwtString) { + // Create SD-JWT instance with minimal configuration (no verification needed for decoding) + const sdjwt = new SDJwtVcInstance({ + signAlg: 'EdDSA', + hasher: digest, + hashAlg: 'sha-256', + saltGenerator: generateSalt, + }); + + // Decode the SD-JWT + return await sdjwt.decode(sdJwtString); +} + +/** + * Verifies an SD-JWT credential + * @param {string} jwt - The SD-JWT string to verify + * @returns {Promise} Verification result with verified status and optional error + * @returns {boolean} returns.verified - Whether the credential is valid + * @returns {string} [returns.error] - Error message if verification failed + */ +export async function verifySDJWT(jwt) { + try { + // Decode the SD-JWT + const decoded = await decodeSDJWT(jwt); + + // Extract payload for validation + const payload: any = decoded.jwt.payload; + + // Check expiration date if present + if (payload.exp) { + const now = Math.floor(Date.now() / 1000); + const exp = Number(payload.exp); + if (now > exp) { + return { + verified: false, + error: 'SD-JWT credential has expired', + }; + } + } + + // Check not-before date if present + if (payload.nbf) { + const now = Math.floor(Date.now() / 1000); + const nbf = Number(payload.nbf); + if (now < nbf) { + return { + verified: false, + error: 'SD-JWT credential is not yet valid', + }; + } + } + + // If we successfully decoded and passed date checks, consider it verified + return { + verified: true, + }; + } catch (error) { + return { + verified: false, + error: error.message || 'Failed to verify SD-JWT credential', + }; + } +} + +/** + * Converts a decoded SD-JWT into W3C Verifiable Credential format + * @param {Object} decodedSDJWT - The decoded SD-JWT object from SDJwtVcInstance.decode() + * @param {string} [encodedSDJWT] - Optional raw encoded SD-JWT string + * @returns {Object} W3C Verifiable Credential format with SD-JWT metadata + */ +export function sdJwtToW3C(decodedSDJWT, encodedSDJWT?) { + const {jwt, disclosures} = decodedSDJWT; + + // The jwt object already has header and payload parsed + const header = jwt.header; + const payload = jwt.payload; + + // Build credential subject from disclosed claims + const credentialSubject: any = {}; + + // Process disclosures to build the credential subject + if (disclosures && Array.isArray(disclosures)) { + disclosures.forEach(disclosure => { + if (disclosure && disclosure.key && disclosure.value !== undefined) { + credentialSubject[disclosure.key] = disclosure.value; + } + }); + } + + // Extract issuer from payload + const issuer = payload.iss || payload.issuer; + + // Extract subject ID if present in disclosures + const subjectId = credentialSubject.id; + + // Build final credential subject with id if available + const finalCredentialSubject = subjectId + ? {id: subjectId, ...credentialSubject} + : credentialSubject; + + // Extract credential type from vct (verifiable credential type) field + // vct is the SD-JWT VC type claim + const credentialType = payload.vct || 'UnknownCredential'; + + // Build the W3C credential + const w3cCredential: any = { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential', credentialType], + issuer: issuer, + credentialSubject: finalCredentialSubject, + }; + + // Add issuance date if available + if (payload.iat) { + w3cCredential.issuanceDate = new Date(payload.iat * 1000).toISOString(); + } + + // Add expiration date if available + if (payload.exp) { + w3cCredential.expirationDate = new Date(payload.exp * 1000).toISOString(); + } + + // Add credential ID if available + if (payload.jti) { + w3cCredential.id = payload.jti; + } + + // Store SD-JWT metadata for unwrapping during presentation flow + // This allows converting back to SD-JWT format when needed + w3cCredential._sd_jwt = { + // Raw encoded SD-JWT string + encoded: encodedSDJWT, + // Decoded SD-JWT structure with all disclosures + decoded: decodedSDJWT, + }; + + return w3cCredential; +} + +/** + * Decodes an SD-JWT string and converts it to W3C credential format + * @param {string} sdJwtString - The SD-JWT string + * @returns {Promise} W3C Verifiable Credential format with SD-JWT metadata + */ +export async function decodeSDJWTToW3C(sdJwtString) { + // Decode the SD-JWT using the reusable decode function + const decoded = await decodeSDJWT(sdJwtString); + + // Convert to W3C format, passing both decoded data and raw string + return sdJwtToW3C(decoded, sdJwtString); +} + +/** + * Converts a credential to W3C format + * Handles both SD-JWT credentials (needs decoding) and regular W3C credentials (returns as-is) + * @param {string|Object} credential - Either an SD-JWT string or a credential object + * @returns {Promise} W3C Verifiable Credential format + */ +export async function credentialToW3C(credential) { + // If it's already an object with a type field, assume it's already W3C format + if (typeof credential === 'object' && credential.type) { + return credential; + } + + // If it's a string, check if it's an SD-JWT + if (typeof credential === 'string') { + // First try to parse as JSON + try { + const parsed = JSON.parse(credential); + if (parsed.type) { + return parsed; + } + } catch (e) { + // Not a JSON string, might be a JWT + } + + // Check if it's an SD-JWT + try { + if (isSDJWTCredential(credential)) { + return await decodeSDJWTToW3C(credential); + } + } catch (e) { + // Not a valid SD-JWT + } + } + + // If it's an object with a data field (like from wallet SDK) + if (typeof credential === 'object' && credential.data) { + return await credentialToW3C(credential.data); + } + + throw new Error('Unable to convert credential to W3C format'); +} diff --git a/packages/wasm/src/services/credential/service.ts b/packages/wasm/src/services/credential/service.ts index f1719884..dc7dd51e 100644 --- a/packages/wasm/src/services/credential/service.ts +++ b/packages/wasm/src/services/credential/service.ts @@ -39,6 +39,7 @@ import axios from 'axios'; import {getIsRevoked, getWitnessDetails} from './bbs-revocation'; import {getPexRequiredAttributes, shouldSkipAttribute} from './pex-helpers'; import {didService} from '../dids/service'; +import {isSDJWTCredential as checkIsSDJWT, credentialToW3C as convertCredentialToW3C, verifySDJWT, createSDJWTPresentation} from './sd-jwt'; /** * PEX (Presentation Exchange) instance for credential filtering @@ -117,8 +118,19 @@ class CredentialService { CredentialService.prototype.deriveVCFromPresentation, CredentialService.prototype.isBBSPlusCredential, CredentialService.prototype.isKvacCredential, + CredentialService.prototype.isSDJWTCredential, + CredentialService.prototype.credentialToW3C, + CredentialService.prototype.createSDJWTPresentation, CredentialService.prototype.acquireOIDCredential, ]; + + + createSDJWTPresentation(params) { + validation.createSDJWTPresentation(params); + const {attributesToReveal, credential} = params; + return createSDJWTPresentation({attributesToReveal, credential}); + } + /** * Generates a new verifiable credential template * @param {Object} [params={}] - Generation parameters @@ -254,7 +266,16 @@ class CredentialService { */ async verifyCredential(params) { validation.verifyCredential(params); - const {credential, membershipWitness} = params; + let {credential, membershipWitness} = params; + + if (credential._sd_jwt) { + credential = credential?._sd_jwt?.encoded; + } + + if (checkIsSDJWT(credential)) { + return verifySDJWT(credential); + } + const result = await verifyCredential(credential, { resolver: blockchainService.resolver, revocationApi: {dock: blockchainService.dock}, @@ -348,6 +369,44 @@ class CredentialService { return isKvacCredential(credential); } + /** + * Checks if a credential is an SD-JWT (Selective Disclosure JWT) credential + * @param {Object} params - Check parameters + * @param {string} params.credential - The JWT string to check + * @returns {boolean} True if the credential is an SD-JWT credential + * @example + * const isSDJWT = credentialService.isSDJWTCredential({ + * credential: 'eyJ0eXAiOiJ2YytzZC1qd3Q...' + * }); + */ + isSDJWTCredential(params) { + const {credential} = params; + return checkIsSDJWT(credential); + } + + /** + * Converts a credential to W3C Verifiable Credential format + * @description Handles both SD-JWT credentials (needs decoding) and regular W3C credentials (returns as-is) + * @param {Object} params - Conversion parameters + * @param {string|Object} params.credential - Either an SD-JWT string or a credential object + * @returns {Promise} W3C Verifiable Credential format + * @throws {Error} If credential cannot be converted to W3C format + * @example + * // Convert SD-JWT to W3C format + * const w3cCredential = await credentialService.credentialToW3C({ + * credential: 'eyJ0eXAiOiJ2YytzZC1qd3Q...' + * }); + * + * // Returns W3C credential as-is + * const w3cCredential = await credentialService.credentialToW3C({ + * credential: { '@context': [...], type: [...], ... } + * }); + */ + async credentialToW3C(params) { + const {credential} = params; + return convertCredentialToW3C(credential); + } + /** * Acquires a credential through OpenID for Verifiable Credentials (OID4VC) * @param {Object} params - Acquisition parameters From 7da23fb572d2198755ac5bb36ed6845de851916c Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 14 Oct 2025 13:19:01 -0300 Subject: [PATCH 2/9] DCKA-5152: improving unit tests --- .../src/services/credential/sd-jwt.test.ts | 263 ++++++++++++++++++ .../wasm/src/services/credential/sd-jwt.ts | 5 - 2 files changed, 263 insertions(+), 5 deletions(-) diff --git a/packages/wasm/src/services/credential/sd-jwt.test.ts b/packages/wasm/src/services/credential/sd-jwt.test.ts index 46793823..0916fe5f 100644 --- a/packages/wasm/src/services/credential/sd-jwt.test.ts +++ b/packages/wasm/src/services/credential/sd-jwt.test.ts @@ -496,4 +496,267 @@ describe('SD-JWT Service', () => { expect(typeof result.error).toBe('string'); }); }); + + describe('createSDJWTPresentation', () => { + it('should create presentation with selective disclosure of specific attributes', async () => { + // Test selective disclosure - reveal only name and date, hide id and number + const presentation = await createSDJWTPresentation({ + attributesToReveal: ['name', 'date'], + credential: TEST_SD_JWT, + }); + + // Presentation should be a string + expect(typeof presentation).toBe('string'); + + // Should be a valid JWT format with disclosures + expect(presentation).toContain('~'); + + // Decode the presentation to verify only requested attributes are included + const decoded = await decodeSDJWT(presentation); + const disclosureKeys = decoded.disclosures.map((d: any) => d.key); + + // Should include requested attributes + expect(disclosureKeys).toContain('name'); + expect(disclosureKeys).toContain('date'); + + // Should NOT include unrequested attributes + expect(disclosureKeys).not.toContain('number'); + // Note: 'id' might be included by default in some implementations + }); + + it('should create presentation revealing only one attribute', async () => { + const presentation = await createSDJWTPresentation({ + attributesToReveal: ['name'], + credential: TEST_SD_JWT, + }); + + expect(typeof presentation).toBe('string'); + + const decoded = await decodeSDJWT(presentation); + const disclosureKeys = decoded.disclosures.map((d: any) => d.key); + + // Should only include the requested attribute + expect(disclosureKeys).toContain('name'); + expect(disclosureKeys.filter((k: string) => k !== 'name').length).toBeLessThan(disclosureKeys.length); + }); + + it('should create presentation revealing all attributes', async () => { + const presentation = await createSDJWTPresentation({ + attributesToReveal: ['id', 'name', 'date', 'number'], + credential: TEST_SD_JWT, + }); + + expect(typeof presentation).toBe('string'); + + const decoded = await decodeSDJWT(presentation); + const disclosureKeys = decoded.disclosures.map((d: any) => d.key); + + // Should include all requested attributes + expect(disclosureKeys).toContain('id'); + expect(disclosureKeys).toContain('name'); + expect(disclosureKeys).toContain('date'); + expect(disclosureKeys).toContain('number'); + }); + + it('should create presentation with empty attributes array (minimal disclosure)', async () => { + const presentation = await createSDJWTPresentation({ + attributesToReveal: [], + credential: TEST_SD_JWT, + }); + + expect(typeof presentation).toBe('string'); + expect(presentation).toContain('~'); + + // Should still be valid JWT format + const decoded = await decodeSDJWT(presentation); + expect(decoded).toHaveProperty('jwt'); + expect(decoded).toHaveProperty('disclosures'); + + // Should have minimal or no disclosures + expect(Array.isArray(decoded.disclosures)).toBe(true); + }); + + it('should maintain credential integrity in presentation', async () => { + const presentation = await createSDJWTPresentation({ + attributesToReveal: ['name', 'date'], + credential: TEST_SD_JWT, + }); + + const decoded = await decodeSDJWT(presentation); + + // JWT payload should maintain core claims + expect(decoded.jwt.payload).toHaveProperty('iss'); + expect(decoded.jwt.payload).toHaveProperty('vct'); + expect(decoded.jwt.payload.vct).toBe('InternalTesting'); + expect(decoded.jwt.payload.iss).toBe('did:cheqd:testnet:c0890f1c-c7bb-4ea6-be7a-8c31404743b7#keys-1'); + }); + + it('should convert presentation to W3C format correctly', async () => { + const presentation = await createSDJWTPresentation({ + attributesToReveal: ['name', 'number'], + credential: TEST_SD_JWT, + }); + + // Convert presentation to W3C format + const w3cPresentation = await decodeSDJWTToW3C(presentation) as any; + + expect(w3cPresentation).toHaveProperty('credentialSubject'); + expect(w3cPresentation.credentialSubject).toHaveProperty('name'); + expect(w3cPresentation.credentialSubject).toHaveProperty('number'); + + // Should have the revealed values + expect(w3cPresentation.credentialSubject.name).toBe('maycon'); + expect(w3cPresentation.credentialSubject.number).toBe(123); + }); + + it('should handle non-existent attribute gracefully', async () => { + // Request an attribute that doesn't exist in the credential + const presentation = await createSDJWTPresentation({ + attributesToReveal: ['name', 'nonExistentAttribute'], + credential: TEST_SD_JWT, + }); + + expect(typeof presentation).toBe('string'); + + const decoded = await decodeSDJWT(presentation); + const disclosureKeys = decoded.disclosures.map((d: any) => d.key); + + // Should include existing attributes + expect(disclosureKeys).toContain('name'); + // Non-existent attribute should not break the process + expect(presentation).toBeTruthy(); + }); + + it('should fail with invalid credential format', async () => { + await expect( + createSDJWTPresentation({ + attributesToReveal: ['name'], + credential: 'invalid-jwt-format', + }) + ).rejects.toThrow(); + }); + + it('should fail with empty credential', async () => { + await expect( + createSDJWTPresentation({ + attributesToReveal: ['name'], + credential: '', + }) + ).rejects.toThrow(); + }); + + it('should preserve presentation format for verification flow', async () => { + const presentation = await createSDJWTPresentation({ + attributesToReveal: ['name', 'date'], + credential: TEST_SD_JWT, + }); + + // The presentation should be verifiable + const verificationResult = await verifySDJWT(presentation); + expect(verificationResult.verified).toBe(true); + }); + }); + + describe('Presentation Integration Tests', () => { + it('should complete full flow: store credential -> create presentation -> verify', async () => { + // Step 1: Convert SD-JWT to W3C format (as done in addCredential) + const w3cCredential = await decodeSDJWTToW3C(TEST_SD_JWT) as any; + + // Verify metadata is stored + expect(w3cCredential._sd_jwt).toBeDefined(); + expect(w3cCredential._sd_jwt.encoded).toBe(TEST_SD_JWT); + + // Step 2: Create presentation from stored credential (as done in verification-controller) + const presentation = await createSDJWTPresentation({ + attributesToReveal: ['name', 'date'], + credential: w3cCredential._sd_jwt.encoded, + }); + + expect(typeof presentation).toBe('string'); + + // Step 3: Verify presentation + const verificationResult = await verifySDJWT(presentation); + expect(verificationResult.verified).toBe(true); + + // Step 4: Decode presentation to check disclosed claims + const decoded = await decodeSDJWT(presentation); + const disclosureKeys = decoded.disclosures.map((d: any) => d.key); + + expect(disclosureKeys).toContain('name'); + expect(disclosureKeys).toContain('date'); + }); + + it('should support multiple presentation creations from same credential', async () => { + // Create first presentation with some attributes + const presentation1 = await createSDJWTPresentation({ + attributesToReveal: ['name'], + credential: TEST_SD_JWT, + }); + + // Create second presentation with different attributes + const presentation2 = await createSDJWTPresentation({ + attributesToReveal: ['date', 'number'], + credential: TEST_SD_JWT, + }); + + // Both should be valid + expect(typeof presentation1).toBe('string'); + expect(typeof presentation2).toBe('string'); + + // They should be different (different disclosures) + expect(presentation1).not.toBe(presentation2); + + // Both should verify + const result1 = await verifySDJWT(presentation1); + const result2 = await verifySDJWT(presentation2); + + expect(result1.verified).toBe(true); + expect(result2.verified).toBe(true); + + // Check different attributes are disclosed + const decoded1 = await decodeSDJWT(presentation1); + const decoded2 = await decodeSDJWT(presentation2); + + const keys1 = decoded1.disclosures.map((d: any) => d.key); + const keys2 = decoded2.disclosures.map((d: any) => d.key); + + expect(keys1).toContain('name'); + expect(keys2).toContain('date'); + expect(keys2).toContain('number'); + }); + + it('should match verification-controller usage pattern', async () => { + // Simulate the exact pattern used in verification-controller.ts + + // Credential is stored in wallet as W3C with _sd_jwt metadata + const storedCredential = await decodeSDJWTToW3C(TEST_SD_JWT) as any; + + // During presentation, extract encoded SD-JWT and create selective disclosure + const credentialSelection = { + credential: storedCredential, + attributesToReveal: ['name', 'date'], + }; + + // This is the exact pattern from verification-controller.ts:172-179 + if (credentialSelection.credential._sd_jwt) { + const derivedCredential = await createSDJWTPresentation({ + attributesToReveal: credentialSelection.attributesToReveal, + credential: credentialSelection.credential._sd_jwt.encoded, + }); + + // Verify the presentation is a string (not an array) + expect(typeof derivedCredential).toBe('string'); + + // Verify it can be decoded + const decoded = await decodeSDJWT(derivedCredential); + expect(decoded).toHaveProperty('jwt'); + expect(decoded).toHaveProperty('disclosures'); + + // Verify disclosed attributes + const disclosedKeys = decoded.disclosures.map((d: any) => d.key); + expect(disclosedKeys).toContain('name'); + expect(disclosedKeys).toContain('date'); + } + }); + }); }); diff --git a/packages/wasm/src/services/credential/sd-jwt.ts b/packages/wasm/src/services/credential/sd-jwt.ts index 2ef2fc44..a6ae2fc0 100644 --- a/packages/wasm/src/services/credential/sd-jwt.ts +++ b/packages/wasm/src/services/credential/sd-jwt.ts @@ -229,10 +229,5 @@ export async function credentialToW3C(credential) { } } - // If it's an object with a data field (like from wallet SDK) - if (typeof credential === 'object' && credential.data) { - return await credentialToW3C(credential.data); - } - throw new Error('Unable to convert credential to W3C format'); } From e7fea6dc954b95770736e4721fa501876dcb4b80 Mon Sep 17 00:00:00 2001 From: Maycon Date: Thu, 16 Oct 2025 13:13:01 -0300 Subject: [PATCH 3/9] fixing integration tests --- packages/wasm/src/services/credential/service.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/wasm/src/services/credential/service.ts b/packages/wasm/src/services/credential/service.ts index dc7dd51e..4bb76c4f 100644 --- a/packages/wasm/src/services/credential/service.ts +++ b/packages/wasm/src/services/credential/service.ts @@ -126,7 +126,6 @@ class CredentialService { createSDJWTPresentation(params) { - validation.createSDJWTPresentation(params); const {attributesToReveal, credential} = params; return createSDJWTPresentation({attributesToReveal, credential}); } @@ -210,8 +209,16 @@ class CredentialService { const {credentials, keyDoc, challenge, id, domain} = params; const vp = new VerifiablePresentation(id); let shouldSkipSigning = false; + let jwtCredentials = []; + for (const signedVC of credentials) { - vp.addCredential(signedVC); + + if (typeof signedVC === 'string') { + jwtCredentials.push(signedVC); + shouldSkipSigning = true; + } else { + vp.addCredential(signedVC); + } shouldSkipSigning = shouldSkipSigning || isAnnonymousCredential(signedVC); } @@ -224,7 +231,9 @@ class CredentialService { const suite = await getSuiteFromKeyDoc(keyPair); if (shouldSkipSigning) { - return vp.toJSON(); + const result = vp.toJSON(); + result.verifiableCredential.push(...jwtCredentials); + return result; } return vp.sign(suite, challenge, domain, blockchainService.resolver); From 573e7ce2a7c6a5b79215a5e56e37ab4b8c0bbb76 Mon Sep 17 00:00:00 2001 From: Maycon Date: Thu, 16 Oct 2025 13:43:05 -0300 Subject: [PATCH 4/9] fixing integration tests --- integration-tests/sd-jwt.test.ts | 90 +++++++++++++++++++ .../src/services/credential/sd-jwt.test.ts | 46 +--------- .../wasm/src/services/credential/sd-jwt.ts | 2 - 3 files changed, 91 insertions(+), 47 deletions(-) create mode 100644 integration-tests/sd-jwt.test.ts diff --git a/integration-tests/sd-jwt.test.ts b/integration-tests/sd-jwt.test.ts new file mode 100644 index 00000000..f16c2586 --- /dev/null +++ b/integration-tests/sd-jwt.test.ts @@ -0,0 +1,90 @@ +import { + BasicCredential, + PolygonIDCredential, + UniversityDegreeCredential, + UniversityDegreeCredentialBBS, +} from './data/credentials'; +import { + cleanup, + closeWallet, + getCredentialProvider, + getWallet, +} from './helpers'; +import {ProofTemplateIds, createProofRequest} from './helpers/certs-helpers'; +import {createVerificationController} from '@docknetwork/wallet-sdk-core/src/verification-controller'; + + +describe('SD JWT Credentials', () => { + let wallet; + let credentialId; + const jwt = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJraWQiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDpjMDg5MGYxYy1jN2JiLTRlYTYtYmU3YS04YzMxNDA0NzQzYjcja2V5cy0xIiwiYWxnIjoiRWREU0EifQ.eyJpYXQiOjE3NTk0MTQzOTQsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OmMwODkwZjFjLWM3YmItNGVhNi1iZTdhLThjMzE0MDQ3NDNiNyNrZXlzLTEiLCJ2Y3QiOiJJbnRlcm5hbFRlc3RpbmciLCJfc2QiOlsiM3JVUGt1Mk5XckFFeTV3ZE9uVms5TkJBa0haMWE4RDB6Y2liMFNQdmthWSIsIkhjNHAxMnZTTWNMQ0piNVRYVlFrdFFMR2xjM0J3SDNWN2ltakV5ZDdvdzAiLCJNZWNNZEd6NjAxY3kwTTdvanRtSjR1LUI5LTJxSXAya1RvbFpDUm1GZ1pFIiwiZmo4WHdBb0lERmRsWmpEa0NTVzVpeXBPYUZBcVplTWZDRncwTWd3cHhOQSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.FcYvNrldceL5BTNmoIaS4Mub8a5NcbiseUeSmmvUpOW8SUom-bchV5AEefrH1VMECbdc2whhk2sW4_jmZo7_Dw~WyI1YTRkZWY1MTAzYjRiYjc0IiwiaWQiLCJkaWQ6a2V5Ono2TWt1OVI4emRBOExENmhjRlhrbjQ3akxuZmNLWk5HbXdhVHJEbmFDQmtTYjhVbiJd~WyJiZDUyMjQ5ZjAyNjk3NWRkIiwiZGF0ZSIsIjIwMjUtMTAtMDkiXQ~WyJlMjcxNzExNzVkODdlMWE1IiwibmFtZSIsIm1heWNvbiJd~WyIwZDFiOTBlNTlhNmMyNDNiIiwibnVtYmVyIiwxMjNd~'; + + beforeAll(async () => { + await cleanup(); + + wallet = await getWallet(); + + const result = await getCredentialProvider().addCredential(jwt); + credentialId = result.id; + }); + + it('expect to import SD-JWT credential', async () => { + const credential = await getCredentialProvider().getById(credentialId); + + expect(credential).toBeDefined(); + expect(credential._sd_jwt).toBeDefined(); + expect(credential._sd_jwt.encoded).toBe(jwt); + + expect(credential.type).toEqual([ + 'VerifiableCredential', + 'InternalTesting', + ]); + expect(credential.issuer).toBe( + 'did:cheqd:testnet:c0890f1c-c7bb-4ea6-be7a-8c31404743b7#keys-1', + ); + expect(credential.credentialSubject.number).toBe(123); + expect(credential.issuanceDate).toBe('2025-10-02T14:13:14.000Z'); + expect(credential.expirationDate).toBe(undefined); + }); + + it('expect to create presentation from SD-JWT credential', async () => { + const credential = await getCredentialProvider().getById(credentialId); + + const proofRequest = await createProofRequest( + ProofTemplateIds.ANY_CREDENTIAL, + ); + + const controller = await createVerificationController({ + wallet, + }); + + await controller.start({ + template: proofRequest, + }); + + let attributesToReveal = ['credentialSubject.name']; + + controller.selectedCredentials.set(credential.id, { + credential: credential, + attributesToReveal, + }); + + const presentation = await controller.createPresentation(); + + console.log(JSON.stringify(presentation, null, 2)); + + let certsResponse; + try { + certsResponse = await controller.submitPresentation(presentation); + console.log('CERTS response'); + console.log(JSON.stringify(certsResponse, null, 2)); + } catch (err) { + certsResponse = err.response.data; + console.log('Certs API returned an error'); + console.log(JSON.stringify(certsResponse, null, 2)); + } + }); + + afterAll(() => closeWallet()); +}); diff --git a/packages/wasm/src/services/credential/sd-jwt.test.ts b/packages/wasm/src/services/credential/sd-jwt.test.ts index 0916fe5f..641b4ea2 100644 --- a/packages/wasm/src/services/credential/sd-jwt.test.ts +++ b/packages/wasm/src/services/credential/sd-jwt.test.ts @@ -91,7 +91,6 @@ describe('SD-JWT Service', () => { id: 'credential-id-123', _sd_jwt: { encoded: undefined, - decoded: decodedSDJWT, }, }); }); @@ -118,7 +117,6 @@ describe('SD-JWT Service', () => { expect(result._sd_jwt).toBeDefined(); expect(result._sd_jwt.encoded).toBe(encodedSDJWT); - expect(result._sd_jwt.decoded).toEqual(decodedSDJWT); }); it('should handle minimal SD-JWT without optional fields', () => { @@ -149,7 +147,6 @@ describe('SD-JWT Service', () => { }, _sd_jwt: { encoded: undefined, - decoded: decodedSDJWT, }, }); }); @@ -277,20 +274,9 @@ describe('SD-JWT Service', () => { // Verify _sd_jwt metadata is present expect(result).toHaveProperty('_sd_jwt'); expect(result._sd_jwt).toHaveProperty('encoded'); - expect(result._sd_jwt).toHaveProperty('decoded'); // Verify raw SD-JWT string is stored expect(result._sd_jwt.encoded).toBe(TEST_SD_JWT); - - // Verify decoded structure contains expected fields - expect(result._sd_jwt.decoded).toHaveProperty('jwt'); - expect(result._sd_jwt.decoded).toHaveProperty('disclosures'); - expect(result._sd_jwt.decoded.jwt).toHaveProperty('header'); - expect(result._sd_jwt.decoded.jwt).toHaveProperty('payload'); - - // Verify we can access disclosure information - expect(Array.isArray(result._sd_jwt.decoded.disclosures)).toBe(true); - expect(result._sd_jwt.decoded.disclosures.length).toBeGreaterThan(0); }); it('should handle SD-JWT with minimal disclosures', async () => { @@ -322,25 +308,6 @@ describe('SD-JWT Service', () => { expect(result).toEqual(W3C_CREDENTIAL); }); - it('should handle credential object with data field', async () => { - const credentialWrapper = { - data: W3C_CREDENTIAL, - }; - const result = await credentialToW3C(credentialWrapper); - expect(result).toEqual(W3C_CREDENTIAL); - }); - - it('should handle nested credential object with SD-JWT in data field', async () => { - const credentialWrapper = { - data: TEST_SD_JWT, - }; - const result = await credentialToW3C(credentialWrapper); - - expect(result).toHaveProperty('@context'); - expect(result).toHaveProperty('type'); - expect(result.type).toEqual(['VerifiableCredential', 'InternalTesting']); - }); - it('should throw error for unsupported credential format', async () => { const invalidCredential = 'not-a-valid-jwt-or-credential'; @@ -353,7 +320,7 @@ describe('SD-JWT Service', () => { await expect(credentialToW3C('')).rejects.toThrow(); }); - it('should handle object without type or data field', async () => { + it('should handle object without type field', async () => { const invalidObject = { someField: 'someValue', }; @@ -363,15 +330,6 @@ describe('SD-JWT Service', () => { ); }); - it('should parse JSON string containing credential with data field', async () => { - const jsonString = JSON.stringify({ - data: W3C_CREDENTIAL, - }); - const parsedObject = JSON.parse(jsonString); - const result = await credentialToW3C(parsedObject); - expect(result).toEqual(W3C_CREDENTIAL); - }); - it('should handle regular JWT (not SD-JWT)', async () => { await expect(credentialToW3C(REGULAR_JWT)).rejects.toThrow( 'Unable to convert credential to W3C format' @@ -403,7 +361,6 @@ describe('SD-JWT Service', () => { // Verify SD-JWT metadata is stored for unwrapping expect(w3cCredential._sd_jwt).toBeDefined(); expect(w3cCredential._sd_jwt.encoded).toBe(TEST_SD_JWT); - expect(w3cCredential._sd_jwt.decoded).toBeDefined(); }); it('should handle credentialToW3C with the test SD-JWT', async () => { @@ -416,7 +373,6 @@ describe('SD-JWT Service', () => { // Verify SD-JWT metadata for presentation unwrapping expect(result._sd_jwt).toBeDefined(); expect(result._sd_jwt.encoded).toBe(TEST_SD_JWT); - expect(result._sd_jwt.decoded).toBeDefined(); }); }); diff --git a/packages/wasm/src/services/credential/sd-jwt.ts b/packages/wasm/src/services/credential/sd-jwt.ts index a6ae2fc0..b1aeca09 100644 --- a/packages/wasm/src/services/credential/sd-jwt.ts +++ b/packages/wasm/src/services/credential/sd-jwt.ts @@ -175,8 +175,6 @@ export function sdJwtToW3C(decodedSDJWT, encodedSDJWT?) { w3cCredential._sd_jwt = { // Raw encoded SD-JWT string encoded: encodedSDJWT, - // Decoded SD-JWT structure with all disclosures - decoded: decodedSDJWT, }; return w3cCredential; From 49d2638085ad06a038b16a8dce5e417745131ecb Mon Sep 17 00:00:00 2001 From: Maycon Date: Fri, 17 Oct 2025 13:52:25 -0300 Subject: [PATCH 5/9] fixing tests --- packages/wasm/src/services/credential/config.ts | 8 +++++++- packages/wasm/src/services/credential/service-rpc.js | 9 +++++++++ packages/wasm/src/services/credential/service.ts | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/wasm/src/services/credential/config.ts b/packages/wasm/src/services/credential/config.ts index ca1e1da0..1b0185ab 100644 --- a/packages/wasm/src/services/credential/config.ts +++ b/packages/wasm/src/services/credential/config.ts @@ -12,7 +12,13 @@ export const validation = { }, verifyCredential: params => { const {credential} = params; - assert(typeof credential === 'object', 'invalid credential'); + assert( + typeof credential === 'object' || typeof credential === 'string', + 'invalid credential', + ); + if (typeof credential === 'object') { + assert(Object.keys(credential).length > 0, 'invalid credential'); + } }, createBBSPresentation: params => { const {credentials} = params; diff --git a/packages/wasm/src/services/credential/service-rpc.js b/packages/wasm/src/services/credential/service-rpc.js index ca5e30e7..6b17b005 100644 --- a/packages/wasm/src/services/credential/service-rpc.js +++ b/packages/wasm/src/services/credential/service-rpc.js @@ -29,6 +29,15 @@ export class CredentialServiceRPC extends RpcService { async isKvacCredential(params) { return this.call('isKvacCredential', params); } + async isSDJWTCredential(params) { + return this.call('isSDJWTCredential', params); + } + async credentialToW3C(params) { + return this.call('credentialToW3C', params); + } + async createSDJWTPresentation(params) { + return this.call('createSDJWTPresentation', params); + } async getAccumulatorId(params) { return this.call('getAccumulatorId', params); } diff --git a/packages/wasm/src/services/credential/service.ts b/packages/wasm/src/services/credential/service.ts index 4bb76c4f..8423dca6 100644 --- a/packages/wasm/src/services/credential/service.ts +++ b/packages/wasm/src/services/credential/service.ts @@ -281,7 +281,7 @@ class CredentialService { credential = credential?._sd_jwt?.encoded; } - if (checkIsSDJWT(credential)) { + if (typeof credential === 'string' && checkIsSDJWT(credential)) { return verifySDJWT(credential); } From b3340daafb638fbf29249ca5f15c7d2cd5fa9edc Mon Sep 17 00:00:00 2001 From: Maycon Date: Fri, 17 Oct 2025 14:10:35 -0300 Subject: [PATCH 6/9] fixing cloud wallet tests --- integration-tests/cloud-wallet.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/integration-tests/cloud-wallet.test.ts b/integration-tests/cloud-wallet.test.ts index b515709a..b96d5be5 100644 --- a/integration-tests/cloud-wallet.test.ts +++ b/integration-tests/cloud-wallet.test.ts @@ -54,14 +54,16 @@ describe('Cloud wallet', () => { authKey: EDV_AUTH_KEY, })); - await clearEdvDocuments(); - wallet = await createNewWallet({ dontWaitForNetwork: true, dataStore, }); }); + afterAll(async () => { + await clearEdvDocuments(); + }); + it('should generate a valid master key with mnemonic', async () => { const { mnemonic, masterKey } = await generateCloudWalletMasterKey(); From 73cb05a41124d8e17f09c485cab5ba390beab445 Mon Sep 17 00:00:00 2001 From: Maycon Date: Fri, 17 Oct 2025 14:30:07 -0300 Subject: [PATCH 7/9] fixing cloud wallet tests --- packages/core/src/cloud-wallet.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/core/src/cloud-wallet.ts b/packages/core/src/cloud-wallet.ts index e8c3bde0..a83d60af 100644 --- a/packages/core/src/cloud-wallet.ts +++ b/packages/core/src/cloud-wallet.ts @@ -233,6 +233,13 @@ export async function getKeyMappingMasterKey( equals: { 'content.id': identifier } + }).catch(error => { + if (error.message && error.message.includes('does not exist')) { + logger.error('KeyMappingVault does not exist, skipping find'); + return { documents: [] }; + } + + throw error; }); if (!result.documents || result.documents.length === 0) { From 6e8237e780e8fdb22f8494cc7c93c29257d52c68 Mon Sep 17 00:00:00 2001 From: Maycon Date: Fri, 17 Oct 2025 14:39:58 -0300 Subject: [PATCH 8/9] fixing cloud wallet tests --- .github/workflows/react-native-example-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/react-native-example-ci.yml b/.github/workflows/react-native-example-ci.yml index 5f2d39f8..ca99ce02 100644 --- a/.github/workflows/react-native-example-ci.yml +++ b/.github/workflows/react-native-example-ci.yml @@ -19,7 +19,7 @@ jobs: node-version: '20.2.0' - name: Install dependencies - run: npm ci --force + run: npm install --force working-directory: examples/react-native-example - name: lint From b1ffa54e98228de8a44fcf74c0807cebd6d8c7fc Mon Sep 17 00:00:00 2001 From: Maycon Date: Fri, 17 Oct 2025 15:41:38 -0300 Subject: [PATCH 9/9] fixing presentation issues + improve tests --- integration-tests/sd-jwt.test.ts | 21 +++++++------------ .../wasm/src/services/credential/sd-jwt.ts | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/integration-tests/sd-jwt.test.ts b/integration-tests/sd-jwt.test.ts index f16c2586..e5087c25 100644 --- a/integration-tests/sd-jwt.test.ts +++ b/integration-tests/sd-jwt.test.ts @@ -1,3 +1,4 @@ +import { decodeSDJWT } from '@docknetwork/wallet-sdk-wasm/src/services/credential/sd-jwt'; import { BasicCredential, PolygonIDCredential, @@ -63,7 +64,7 @@ describe('SD JWT Credentials', () => { template: proofRequest, }); - let attributesToReveal = ['credentialSubject.name']; + let attributesToReveal = ['credentialSubject.number']; controller.selectedCredentials.set(credential.id, { credential: credential, @@ -72,18 +73,12 @@ describe('SD JWT Credentials', () => { const presentation = await controller.createPresentation(); - console.log(JSON.stringify(presentation, null, 2)); - - let certsResponse; - try { - certsResponse = await controller.submitPresentation(presentation); - console.log('CERTS response'); - console.log(JSON.stringify(certsResponse, null, 2)); - } catch (err) { - certsResponse = err.response.data; - console.log('Certs API returned an error'); - console.log(JSON.stringify(certsResponse, null, 2)); - } + + const decoded = await decodeSDJWT(presentation.verifiableCredential[0]); + + expect(decoded.disclosures?.length).toBe(1); + expect(decoded.disclosures?.[0].key).toBe('number'); + expect(decoded.disclosures?.[0].value).toBe(123); }); afterAll(() => closeWallet()); diff --git a/packages/wasm/src/services/credential/sd-jwt.ts b/packages/wasm/src/services/credential/sd-jwt.ts index b1aeca09..3d7bd622 100644 --- a/packages/wasm/src/services/credential/sd-jwt.ts +++ b/packages/wasm/src/services/credential/sd-jwt.ts @@ -29,7 +29,7 @@ export async function createSDJWTPresentation({ // The list of presented claims must be a subset of the disclosed claims const presentationFrame: any = {}; attributesToReveal.forEach(attribute => { - presentationFrame[attribute] = true; + presentationFrame[attribute.replace('credentialSubject.', '')] = true; }); // Holder creates a presentation using the issued credential and the presentation frame