Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
435 changes: 274 additions & 161 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@babel/preset-env": "^7.22.10",
"@docknetwork/cheqd-blockchain-api": "6.0.1",
"@docknetwork/cheqd-blockchain-modules": "6.0.1",
"@docknetwork/credential-sdk": "0.56.1",
"@docknetwork/credential-sdk": "0.56.2",
"@docknetwork/universal-wallet": "2.1.1",
"@react-native-community/eslint-config": "^2.0.0",
"@testing-library/jest-dom": "^5.11.4",
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/credential-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ export async function isValid({

const {verified, error} = verificationResult;

const errorMessage =
typeof error === 'string'
? error
: error?.message || error?.errors?.[0]?.message || error;

Comment on lines +115 to +119
if (error) {
const sdkNotInitialized = error?.errors?.find(err => err?.message === 'SDK is not initialized');
if (sdkNotInitialized) {
Expand All @@ -122,17 +127,17 @@ export async function isValid({
}

if (!verified) {
const errorMessage = (error?.message || error || '').toString().toLowerCase();
if (errorMessage.includes('revok')) {
const normalizedError = (errorMessage || '').toString().toLowerCase();
if (normalizedError.includes('revok')) {
return {
status: CredentialStatus.Revoked,
error,
error: errorMessage,
};
}

return {
status: CredentialStatus.Invalid,
error: error,
error: errorMessage,
};
}

Expand Down
7 changes: 4 additions & 3 deletions packages/wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@
"@cosmjs/proto-signing": "^0.32.4",
"@docknetwork/cheqd-blockchain-api": "6.0.1",
"@docknetwork/cheqd-blockchain-modules": "6.0.1",
"@docknetwork/credential-sdk": "0.56.1",
"@docknetwork/credential-sdk": "0.56.2",
"@docknetwork/crypto-utils": "0.2.2",
"@docknetwork/universal-wallet": "^2.0.1",
Comment on lines +26 to 28
"@docknetwork/vc-delegation-engine": "1.0.4",
"@docknetwork/wallet-sdk-dids": "^1.9.1",
"@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",
"@sd-jwt/crypto-nodejs": "0.9.2",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any issue with using 0.15.0?

"@sd-jwt/sd-jwt-vc": "0.9.2",
Comment on lines +33 to +34
"@sphereon/oid4vci-client": "^0.16.0",
"@sphereon/oid4vci-common": "^0.16.0",
"@sphereon/pex": "^1.1.4",
Expand Down
16 changes: 16 additions & 0 deletions packages/wasm/src/services/credential/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ describe('Credential Service', () => {
const error0 = await getPromiseError(() => service.verifyCredential({}));
expect(error0.message).toBe('invalid credential');
});
it('expect to reject SD-JWT credentials missing compact serialization', async () => {
const result = await service.verifyCredential({
credential: {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiableCredential', 'BasicCredential'],
issuer: 'did:example:issuer',
credentialSubject: {},
_sd_jwt: {
// decoded-payload imports store metadata without an encoded JWT
},
},
});

expect(result.verified).toBe(false);
expect(result.error).toContain('compact serialization');
});
it('expect to verify credential', async () => {
jest
.spyOn(serviceCredentialUtils, 'verifyCredential')
Expand Down
185 changes: 153 additions & 32 deletions packages/wasm/src/services/credential/sd-jwt.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// @ts-nocheck
import {createJwsSigner, Ed25519Keypair} from '@docknetwork/crypto-utils';
import {digest, generateSalt} from '@sd-jwt/crypto-nodejs';
import {SDJwtVcInstance} from '@sd-jwt/sd-jwt-vc';
import {
isSDJWTCredential,
sdJwtToW3C,
Expand All @@ -11,6 +15,41 @@ import {
// Test SD-JWT credential provided by the user
const TEST_SD_JWT = 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJraWQiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDpjMDg5MGYxYy1jN2JiLTRlYTYtYmU3YS04YzMxNDA0NzQzYjcja2V5cy0xIiwiYWxnIjoiRWREU0EifQ.eyJpYXQiOjE3NTk0MTQzOTQsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OmMwODkwZjFjLWM3YmItNGVhNi1iZTdhLThjMzE0MDQ3NDNiNyNrZXlzLTEiLCJ2Y3QiOiJJbnRlcm5hbFRlc3RpbmciLCJfc2QiOlsiM3JVUGt1Mk5XckFFeTV3ZE9uVms5TkJBa0haMWE4RDB6Y2liMFNQdmthWSIsIkhjNHAxMnZTTWNMQ0piNVRYVlFrdFFMR2xjM0J3SDNWN2ltakV5ZDdvdzAiLCJNZWNNZEd6NjAxY3kwTTdvanRtSjR1LUI5LTJxSXAya1RvbFpDUm1GZ1pFIiwiZmo4WHdBb0lERmRsWmpEa0NTVzVpeXBPYUZBcVplTWZDRncwTWd3cHhOQSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.FcYvNrldceL5BTNmoIaS4Mub8a5NcbiseUeSmmvUpOW8SUom-bchV5AEefrH1VMECbdc2whhk2sW4_jmZo7_Dw~WyI1YTRkZWY1MTAzYjRiYjc0IiwiaWQiLCJkaWQ6a2V5Ono2TWt1OVI4emRBOExENmhjRlhrbjQ3akxuZmNLWk5HbXdhVHJEbmFDQmtTYjhVbiJd~WyJiZDUyMjQ5ZjAyNjk3NWRkIiwiZGF0ZSIsIjIwMjUtMTAtMDkiXQ~WyJlMjcxNzExNzVkODdlMWE1IiwibmFtZSIsIm1heWNvbiJd~WyIwZDFiOTBlNTlhNmMyNDNiIiwibnVtYmVyIiwxMjNd~';

const ISSUER_DID = 'did:ex:issuer#keys-1';

async function issueTestSdJwt(keypair, claims, disclosureFrame = {_sd: ['name', 'date', 'number', 'id']}) {
const signer = createJwsSigner(keypair);
const sdjwt = new SDJwtVcInstance({
signer: async data => {
const bytes = typeof data === 'string' ? Buffer.from(data) : data;
const signature = await signer.sign({data: bytes});
return Buffer.from(signature).toString('base64url');
},
signAlg: 'EdDSA',
hasher: digest,
hashAlg: 'sha-256',
saltGenerator: generateSalt,
});

return sdjwt.issue(claims, disclosureFrame, {
header: {
typ: 'vc+sd-jwt',
alg: 'EdDSA',
kid: claims.iss,
},
});
}

function createIssuerKeypair() {
return Ed25519Keypair.fromSeed(
Uint8Array.from({length: Ed25519Keypair.SeedSize}, (_, index) => index + 1),
);
}

function createKeyResolver(keypair) {
return async () => keypair.publicKey();
}

// Regular JWT (not SD-JWT) for testing
const REGULAR_JWT = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';

Expand Down Expand Up @@ -402,32 +441,69 @@ describe('SD-JWT Service', () => {
});

describe('verifySDJWT', () => {
it('should verify valid SD-JWT successfully', async () => {
const result = await verifySDJWT(TEST_SD_JWT);
it('should fail when no issuer key resolution options are provided', async () => {
const keypair = createIssuerKeypair();
const credential = await issueTestSdJwt(
keypair,
{
iss: ISSUER_DID,
iat: 1700000000,
vct: 'InternalTesting',
name: 'maycon',
},
{_sd: ['name']},
);

expect(result).toHaveProperty('verified');
expect(result.verified).toBe(true);
expect(result.error).toBeUndefined();
// Production callers must pass documentLoader/resolver/keyResolver.
const result = await verifySDJWT(credential);

expect(result.verified).toBe(false);
expect(result.error).toBeDefined();
});

it('should fail verification for expired SD-JWT', async () => {
// Create an expired SD-JWT (exp in the past)
const expiredJWT = 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIiLCJ2Y3QiOiJUZXN0Q3JlZGVudGlhbCIsImV4cCI6MTYwOTQ1OTIwMCwiaWF0IjoxNjA5NDU5MjAwfQ.signature~';
it('should cryptographically verify a valid SD-JWT with keyResolver', async () => {
const keypair = createIssuerKeypair();
const credential = await issueTestSdJwt(keypair, {
iss: ISSUER_DID,
iat: 1700000000,
exp: 1800000000,
vct: 'InternalTesting',
id: 'did:example:subject',
name: 'maycon',
date: '2025-10-09',
number: 123,
});

const result = await verifySDJWT(expiredJWT);
const result = await verifySDJWT(credential, ['name'], {
keyResolver: createKeyResolver(keypair),
});

expect(result.verified).toBe(false);
expect(result.error).toContain('expired');
expect(result.verified).toBe(true);
expect(result.error).toBeUndefined();
expect(result.credentialResults[0]).toMatchObject({
issuer: 'did:ex:issuer',
type: ['InternalTesting'],
credentialSubject: {
name: 'maycon',
},
});
});

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~';
it('should fail verification when issuer key cannot be resolved', async () => {
const keypair = createIssuerKeypair();
const credential = await issueTestSdJwt(keypair, {
iss: ISSUER_DID,
iat: 1700000000,
vct: 'InternalTesting',
name: 'maycon',
}, {_sd: ['name']});

const result = await verifySDJWT(futureJWT);
const result = await verifySDJWT(credential, undefined, {
keyResolver: async () => null,
});

expect(result.verified).toBe(false);
expect(result.error).toContain('not yet valid');
expect(result.error).toBeDefined();
});

it('should fail verification for invalid SD-JWT format', async () => {
Expand All @@ -437,11 +513,24 @@ describe('SD-JWT Service', () => {
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);
it('should fail verification with wrong issuer key', async () => {
const issuerKeypair = createIssuerKeypair();
const wrongKeypair = Ed25519Keypair.fromSeed(
Uint8Array.from({length: Ed25519Keypair.SeedSize}, (_, index) => index + 9),
);
const credential = await issueTestSdJwt(issuerKeypair, {
iss: ISSUER_DID,
iat: 1700000000,
vct: 'InternalTesting',
name: 'maycon',
}, {_sd: ['name']});

const result = await verifySDJWT(credential, undefined, {
keyResolver: createKeyResolver(wrongKeypair),
});

expect(result.verified).toBe(true);
expect(result.verified).toBe(false);
expect(result.error).toBeDefined();
});

it('should return error message on verification failure', async () => {
Expand Down Expand Up @@ -602,25 +691,44 @@ describe('SD-JWT Service', () => {
});

it('should preserve presentation format for verification flow', async () => {
const keypair = createIssuerKeypair();
const credential = await issueTestSdJwt(keypair, {
iss: ISSUER_DID,
iat: 1700000000,
vct: 'InternalTesting',
name: 'maycon',
date: '2025-10-09',
}, {_sd: ['name', 'date']});

const presentation = await createSDJWTPresentation({
attributesToReveal: ['name', 'date'],
credential: TEST_SD_JWT,
credential,
});

// The presentation should be verifiable
const verificationResult = await verifySDJWT(presentation);
const verificationResult = await verifySDJWT(presentation, undefined, {
keyResolver: createKeyResolver(keypair),
});
expect(verificationResult.verified).toBe(true);
});
});

describe('Presentation Integration Tests', () => {
it('should complete full flow: store credential -> create presentation -> verify', async () => {
const keypair = createIssuerKeypair();
const issued = await issueTestSdJwt(keypair, {
iss: ISSUER_DID,
iat: 1700000000,
vct: 'InternalTesting',
name: 'maycon',
date: '2025-10-09',
}, {_sd: ['name', 'date']});

// Step 1: Convert SD-JWT to W3C format (as done in addCredential)
const w3cCredential = await decodeSDJWTToW3C(TEST_SD_JWT) as any;
const w3cCredential = await decodeSDJWTToW3C(issued) as any;

// Verify metadata is stored
expect(w3cCredential._sd_jwt).toBeDefined();
expect(w3cCredential._sd_jwt.encoded).toBe(TEST_SD_JWT);
expect(w3cCredential._sd_jwt.encoded).toBe(issued);

// Step 2: Create presentation from stored credential (as done in verification-controller)
const presentation = await createSDJWTPresentation({
Expand All @@ -630,8 +738,10 @@ describe('SD-JWT Service', () => {

expect(typeof presentation).toBe('string');

// Step 3: Verify presentation
const verificationResult = await verifySDJWT(presentation);
// Step 3: Verify presentation cryptographically
const verificationResult = await verifySDJWT(presentation, undefined, {
keyResolver: createKeyResolver(keypair),
});
expect(verificationResult.verified).toBe(true);

// Step 4: Decode presentation to check disclosed claims
Expand All @@ -643,16 +753,26 @@ describe('SD-JWT Service', () => {
});

it('should support multiple presentation creations from same credential', async () => {
const keypair = createIssuerKeypair();
const credential = await issueTestSdJwt(keypair, {
iss: ISSUER_DID,
iat: 1700000000,
vct: 'InternalTesting',
name: 'maycon',
date: '2025-10-09',
number: 123,
}, {_sd: ['name', 'date', 'number']});

// Create first presentation with some attributes
const presentation1 = await createSDJWTPresentation({
attributesToReveal: ['name'],
credential: TEST_SD_JWT,
credential,
});

// Create second presentation with different attributes
const presentation2 = await createSDJWTPresentation({
attributesToReveal: ['date', 'number'],
credential: TEST_SD_JWT,
credential,
});

// Both should be valid
Expand All @@ -662,9 +782,10 @@ describe('SD-JWT Service', () => {
// They should be different (different disclosures)
expect(presentation1).not.toBe(presentation2);

// Both should verify
const result1 = await verifySDJWT(presentation1);
const result2 = await verifySDJWT(presentation2);
// Both should verify cryptographically
const keyResolver = createKeyResolver(keypair);
const result1 = await verifySDJWT(presentation1, undefined, {keyResolver});
const result2 = await verifySDJWT(presentation2, undefined, {keyResolver});

expect(result1.verified).toBe(true);
expect(result2.verified).toBe(true);
Expand Down
Loading
Loading