Skip to content

Commit aad5b4a

Browse files
authored
integration tests for delegatable credentials (#445)
* testing delegatable credentials * testing delegatable credentials * testing unauthorized scenario * fixing module resolution issue * delegation credential should have rootId as undefined * fixing module resolution issue * should create a valid presentation with a delegated credential
1 parent ef61c31 commit aad5b4a

7 files changed

Lines changed: 950 additions & 283 deletions

File tree

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import {
2+
verifyDelegatablePresentation,
3+
issueDelegationCredential,
4+
issueDelegatedCredential,
5+
createSignedPresentation,
6+
createCedarPolicy,
7+
MAY_CLAIM_IRI,
8+
W3C_CREDENTIALS_V1,
9+
DELEGATION_CONTEXT_TERMS,
10+
} from '@docknetwork/wallet-sdk-wasm/src/services/credential/delegatable-credentials';
11+
import { didService } from '@docknetwork/wallet-sdk-wasm/src/services/dids/service';
12+
13+
// ============================================================================
14+
// TEST-SPECIFIC CONTEXTS (Credit Score Use Case)
15+
// ============================================================================
16+
17+
/**
18+
* Context for credit score delegation credentials
19+
* Extends the base delegation terms with credit score specific vocabulary
20+
*/
21+
const CREDIT_SCORE_DELEGATION_CONTEXT = [
22+
W3C_CREDENTIALS_V1,
23+
{
24+
...DELEGATION_CONTEXT_TERMS,
25+
ex: 'https://example.org/credentials#',
26+
CreditScoreDelegation: 'ex:CreditScoreDelegation',
27+
body: 'ex:body',
28+
},
29+
];
30+
31+
/**
32+
* Context for credit score credentials issued by delegates
33+
*/
34+
const CREDIT_SCORE_CREDENTIAL_CONTEXT = [
35+
W3C_CREDENTIALS_V1,
36+
{
37+
...DELEGATION_CONTEXT_TERMS,
38+
ex: 'https://example.org/credentials#',
39+
xsd: 'http://www.w3.org/2001/XMLSchema#',
40+
CreditScoreCredential: 'ex:CreditScoreCredential',
41+
creditScore: { '@id': 'ex:creditScore', '@type': 'xsd:integer' },
42+
},
43+
];
44+
45+
// ============================================================================
46+
// TEST CONFIGURATION
47+
// ============================================================================
48+
49+
const DELEGATION_ROOT_ID = 'urn:cred:delegation-root';
50+
const CREDIT_SCORE_CRED_ID = 'urn:cred:credit-score-alice';
51+
const SUBJECT_DID = 'did:example:alice';
52+
53+
const CHALLENGE = 'test-challenge-123';
54+
const DOMAIN = 'test.example.com';
55+
56+
describe('Delegatable Credentials', () => {
57+
let rootIssuerKey: any;
58+
let delegateKey: any;
59+
let rootIssuerDid: string;
60+
let delegateDid: string;
61+
let delegationCredential: any;
62+
let creditScoreCredential: any;
63+
let unauthorizedDelegationCredential: any;
64+
65+
beforeAll(async () => {
66+
// Generate key pairs for root issuer and delegate
67+
rootIssuerKey = await didService.generateKeyDoc({
68+
type: 'ed25519',
69+
});
70+
delegateKey = await didService.generateKeyDoc({
71+
type: 'ed25519',
72+
});
73+
74+
// Extract DIDs from the key documents
75+
rootIssuerDid = rootIssuerKey.controller;
76+
delegateDid = delegateKey.controller;
77+
78+
console.log('Root Issuer DID:', rootIssuerDid);
79+
console.log('Delegate DID:', delegateDid);
80+
81+
// Issue the root delegation credential
82+
// This grants the delegate authority to issue creditScore claims
83+
delegationCredential = await issueDelegationCredential(rootIssuerKey, {
84+
id: DELEGATION_ROOT_ID,
85+
issuerDid: rootIssuerDid,
86+
delegateDid: delegateDid,
87+
mayClaim: ['creditScore'],
88+
context: CREDIT_SCORE_DELEGATION_CONTEXT,
89+
types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'],
90+
additionalSubjectProperties: {
91+
body: 'Issuer of Credit Scores',
92+
},
93+
});
94+
95+
// Issue a credit score credential as the delegate
96+
creditScoreCredential = await issueDelegatedCredential(delegateKey, {
97+
id: CREDIT_SCORE_CRED_ID,
98+
issuerDid: delegateDid,
99+
subjectDid: SUBJECT_DID,
100+
claims: {
101+
creditScore: 760,
102+
},
103+
rootCredentialId: DELEGATION_ROOT_ID,
104+
previousCredentialId: DELEGATION_ROOT_ID,
105+
context: CREDIT_SCORE_CREDENTIAL_CONTEXT,
106+
types: ['VerifiableCredential', 'CreditScoreCredential'],
107+
});
108+
109+
// Issue an unauthorized delegation (no creditScore in mayClaim)
110+
unauthorizedDelegationCredential = await issueDelegationCredential(rootIssuerKey, {
111+
id: 'urn:cred:unauthorized-delegation',
112+
issuerDid: rootIssuerDid,
113+
delegateDid: delegateDid,
114+
mayClaim: ['someOtherClaim'], // Does NOT include creditScore
115+
context: CREDIT_SCORE_DELEGATION_CONTEXT,
116+
types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'],
117+
});
118+
});
119+
120+
it('should issue a valid delegation credential', () => {
121+
expect(delegationCredential).toBeDefined();
122+
expect(delegationCredential.id).toBe(DELEGATION_ROOT_ID);
123+
expect(delegationCredential.issuer).toBe(rootIssuerDid);
124+
expect(delegationCredential.credentialSubject.id).toBe(delegateDid);
125+
expect(delegationCredential.credentialSubject[MAY_CLAIM_IRI]).toContain('creditScore');
126+
expect(delegationCredential.proof).toBeDefined();
127+
expect(delegationCredential.rootCredentialId).toBeUndefined();
128+
expect(delegationCredential.previousCredentialId).toBeNull();
129+
});
130+
131+
it('should issue a valid delegated credential', () => {
132+
expect(creditScoreCredential).toBeDefined();
133+
expect(creditScoreCredential.id).toBe(CREDIT_SCORE_CRED_ID);
134+
expect(creditScoreCredential.issuer).toBe(delegateDid);
135+
expect(creditScoreCredential.credentialSubject.id).toBe(SUBJECT_DID);
136+
expect(creditScoreCredential.credentialSubject.creditScore).toBe(760);
137+
expect(creditScoreCredential.proof).toBeDefined();
138+
expect(creditScoreCredential.rootCredentialId).toBe(DELEGATION_ROOT_ID);
139+
expect(creditScoreCredential.previousCredentialId).toBe(DELEGATION_ROOT_ID);
140+
});
141+
142+
it('should create a valid presentation with a delegated credential', async () => {
143+
const presentation = await createSignedPresentation(delegateKey, {
144+
credentials: [delegationCredential],
145+
holderDid: delegateDid,
146+
challenge: CHALLENGE,
147+
domain: DOMAIN,
148+
});
149+
150+
const result = await verifyDelegatablePresentation(presentation, {
151+
challenge: CHALLENGE,
152+
domain: DOMAIN,
153+
});
154+
155+
expect(result.verified).toBe(true);
156+
});
157+
158+
it('should verify authorized delegation with Cedar policies', async () => {
159+
// Create a signed presentation with both credentials
160+
const presentation = await createSignedPresentation(delegateKey, {
161+
credentials: [delegationCredential, creditScoreCredential],
162+
holderDid: delegateDid,
163+
challenge: CHALLENGE,
164+
domain: DOMAIN,
165+
});
166+
167+
// Create Cedar policy that allows this delegation
168+
const policies = createCedarPolicy({
169+
maxDepth: 2,
170+
rootIssuer: rootIssuerDid,
171+
requiredClaims: {
172+
creditScore: 0,
173+
body: 'Issuer of Credit Scores',
174+
},
175+
});
176+
177+
// Verify the presentation
178+
const result = await verifyDelegatablePresentation(presentation, {
179+
challenge: CHALLENGE,
180+
domain: DOMAIN,
181+
policies,
182+
});
183+
184+
console.log('Verification result:', {
185+
verified: result.verified,
186+
delegationDecision: result.delegationResult?.decision,
187+
credentialResults: result.credentialResults?.map(r => r.verified),
188+
});
189+
190+
// Check delegation result
191+
expect(result.delegationResult).toBeDefined();
192+
expect(result.delegationResult?.decision).toBe('allow');
193+
194+
// Log delegation summary for debugging
195+
if (result.delegationResult?.summaries?.length > 0) {
196+
const summary = result.delegationResult.summaries[0];
197+
console.log('Delegation summary:', {
198+
rootIssuer: summary.rootIssuer,
199+
tailIssuer: summary.tailIssuer,
200+
tailDepth: summary.tailDepth,
201+
authorizedClaims: summary.authorizedClaims,
202+
});
203+
}
204+
});
205+
206+
it('should deny unauthorized delegation (wrong mayClaim)', async () => {
207+
// Create a credential that uses an unauthorized delegation
208+
// The unauthorizedDelegationCredential only grants 'someOtherClaim', not 'creditScore'
209+
const unauthorizedCreditScore = await issueDelegatedCredential(delegateKey, {
210+
id: 'urn:cred:unauthorized-credit-score',
211+
issuerDid: delegateDid,
212+
subjectDid: SUBJECT_DID,
213+
claims: {
214+
creditScore: 500,
215+
},
216+
rootCredentialId: 'urn:cred:unauthorized-delegation',
217+
previousCredentialId: 'urn:cred:unauthorized-delegation',
218+
context: CREDIT_SCORE_CREDENTIAL_CONTEXT,
219+
types: ['VerifiableCredential', 'CreditScoreCredential'],
220+
});
221+
222+
// Create presentation with unauthorized delegation
223+
const presentation = await createSignedPresentation(delegateKey, {
224+
credentials: [unauthorizedDelegationCredential, unauthorizedCreditScore],
225+
holderDid: delegateDid,
226+
challenge: CHALLENGE,
227+
domain: DOMAIN,
228+
});
229+
230+
// Create Cedar policy that requires creditScore claim authorization
231+
const policies = createCedarPolicy({
232+
maxDepth: 2,
233+
rootIssuer: rootIssuerDid,
234+
requiredClaims: {
235+
creditScore: 0,
236+
},
237+
});
238+
239+
// Verify the presentation - should fail because creditScore is not authorized
240+
const result = await verifyDelegatablePresentation(presentation, {
241+
challenge: CHALLENGE,
242+
domain: DOMAIN,
243+
policies,
244+
});
245+
246+
console.log('Unauthorized delegation result:', {
247+
verified: result.verified,
248+
delegationDecision: result.delegationResult?.decision,
249+
failures: result.delegationResult?.failures?.map(f => f.message),
250+
});
251+
252+
// Delegation should be denied because the delegate doesn't have creditScore authority
253+
expect(result.delegationResult?.decision).toBe('deny');
254+
expect(result.delegationResult?.failures).toBeDefined();
255+
expect(result.delegationResult?.failures?.length).toBeGreaterThan(0);
256+
expect(result.delegationResult?.failures?.[0]?.code).toBe('UNAUTHORIZED_CLAIM');
257+
});
258+
259+
it('should verify delegation without Cedar policies', async () => {
260+
// Create a signed presentation
261+
const presentation = await createSignedPresentation(delegateKey, {
262+
credentials: [delegationCredential, creditScoreCredential],
263+
holderDid: delegateDid,
264+
challenge: CHALLENGE,
265+
domain: DOMAIN,
266+
});
267+
268+
// Verify without Cedar policies - just validates delegation chain
269+
const result = await verifyDelegatablePresentation(presentation, {
270+
challenge: CHALLENGE,
271+
domain: DOMAIN,
272+
});
273+
274+
console.log('Verification without policies:', {
275+
verified: result.verified,
276+
delegationDecision: result.delegationResult?.decision,
277+
});
278+
279+
expect(result.delegationResult).toBeDefined();
280+
expect(result.delegationResult?.failures || []).toHaveLength(0);
281+
});
282+
283+
it('should create Cedar policies with helper function', () => {
284+
const policy = createCedarPolicy({
285+
maxDepth: 3,
286+
rootIssuer: 'did:example:root',
287+
requiredClaims: {
288+
level: 5,
289+
role: 'admin',
290+
},
291+
});
292+
293+
expect(policy.staticPolicies).toContain('context.tailDepth <= 3');
294+
expect(policy.staticPolicies).toContain('did:example:root');
295+
expect(policy.staticPolicies).toContain('context.authorizedClaims.level >= 5');
296+
expect(policy.staticPolicies).toContain('context.authorizedClaims.role == "admin"');
297+
});
298+
});

jest.config.e2e.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
globalTeardown: './scripts/integration-test-teardown.js',
2727
setupFiles: ['jest-localstorage-mock'],
2828
moduleNameMapper: {
29+
'ky-universal': 'ky',
2930
'@digitalbazaar/edv-client': require.resolve(
3031
'@digitalbazaar/edv-client/main.js',
3132
),
@@ -40,9 +41,8 @@ module.exports = {
4041
'@digitalbazaar/ed25519-verification-key-2018/src/Ed25519VerificationKey2018',
4142
'@digitalbazaar/minimal-cipher': '@digitalbazaar/minimal-cipher/Cipher',
4243
'@digitalbazaar/did-method-key': '@digitalbazaar/did-method-key/lib/main',
43-
'@digitalbazaar/http-client': require.resolve(
44-
'@digitalbazaar/http-client/main.js',
45-
),
44+
'@digitalbazaar/http-client':
45+
'<rootDir>/node_modules/@digitalbazaar/http-client/dist/cjs/index.cjs',
4646
'@docknetwork/wallet-sdk-wasm/lib/(.*)':
4747
'@docknetwork/wallet-sdk-wasm/src/$1',
4848
'@docknetwork/wallet-sdk-data-store/lib/(.*)':
@@ -51,6 +51,6 @@ module.exports = {
5151
'@docknetwork/wallet-sdk-data-store/src',
5252
},
5353
transformIgnorePatterns: [
54-
'/node_modules/(?!@babel|@docknetwork|@digitalbazaar|base58-universal|multiformats|p-limit|yocto-queue|@cheqd/ts-proto)',
54+
'/node_modules/(?!@babel|@docknetwork|@digitalbazaar|base58-universal|multiformats|p-limit|yocto-queue|@cheqd/ts-proto|ky)',
5555
],
5656
};

jest.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ module.exports = {
3232
moduleNameMapper: {
3333
'@digitalbazaar/minimal-cipher': '@digitalbazaar/minimal-cipher/Cipher',
3434
'@digitalbazaar/did-method-key': '@digitalbazaar/did-method-key/lib/main',
35+
'@digitalbazaar/http-client':
36+
'<rootDir>/node_modules/@digitalbazaar/http-client/dist/cjs/index.cjs',
3537
'@docknetwork/wallet-sdk-wasm/lib/(.*)':
3638
'@docknetwork/wallet-sdk-wasm/src/$1',
3739
'@docknetwork/wallet-sdk-data-store/lib/(.*)':

0 commit comments

Comments
 (0)