From 2b33534f9eeec618c7c5bcb298cec309f46274fc Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 23 Dec 2025 14:33:39 -0300 Subject: [PATCH 1/7] testing delegatable credentials --- .../delegatable-credentials.test.ts | 199 ++++++++++ jest.config.e2e.js | 5 +- packages/wasm/package.json | 3 + .../credential/delegatable-credentials.ts | 375 ++++++++++++++++++ 4 files changed, 579 insertions(+), 3 deletions(-) create mode 100644 integration-tests/delegatable-credentials.test.ts create mode 100644 packages/wasm/src/services/credential/delegatable-credentials.ts diff --git a/integration-tests/delegatable-credentials.test.ts b/integration-tests/delegatable-credentials.test.ts new file mode 100644 index 00000000..b78fa4ee --- /dev/null +++ b/integration-tests/delegatable-credentials.test.ts @@ -0,0 +1,199 @@ +import { + verifyDelegatablePresentation, + createCedarPolicy, + createDelegatablePresentation, + createDelegationCredential, + createEd25519Proof, +} from '@docknetwork/wallet-sdk-wasm/src/services/credential/delegatable-credentials'; + + +/** + * Delegation namespace for credential chain properties + */ +const DELEGATION_NAMESPACE = 'https://ld.truvera.io/credentials/delegation#'; + +/** + * Pre-defined context for credit delegation credentials + */ +const CREDIT_DELEGATION_CONTEXT = [ + 'https://www.w3.org/2018/credentials/v1', + 'https://ld.truvera.io/credentials/delegation', + { + '@version': 1.1, + ex: 'https://example.org/credentials#', + delegation: DELEGATION_NAMESPACE, + CreditScoreDelegation: 'ex:CreditScoreDelegation', + DelegationCredential: 'delegation:DelegationCredential', + body: 'ex:body', + rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, + previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, + }, +]; + +/** + * Pre-defined context for credit score credentials + */ +const CREDIT_SCORE_CONTEXT = [ + 'https://www.w3.org/2018/credentials/v1', + 'https://ld.truvera.io/credentials/delegation', + { + '@version': 1.1, + ex: 'https://example.org/credentials#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + delegation: DELEGATION_NAMESPACE, + CreditScoreCredential: 'ex:CreditScoreCredential', + creditScore: { '@id': 'ex:creditScore', '@type': 'xsd:integer' }, + rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, + previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, + }, +]; + +/** + * Pre-defined context for verifiable presentations + */ +const PRESENTATION_CONTEXT = ['https://www.w3.org/2018/credentials/v1']; + +describe('Delegatable Credentials', () => { + // Cedar policy for credit score delegation + const policies = createCedarPolicy({ + maxDepth: 2, + rootIssuer: 'did:dock:a', + requiredClaims: { + creditScore: 0, + body: 'Issuer of Credit Scores', + }, + }); + + // Authorized delegation credential + const authorizedDelegation = createDelegationCredential({ + id: 'urn:cred:deleg-a-b', + context: CREDIT_DELEGATION_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], + issuer: 'did:dock:a', + subjectId: 'did:dock:b', + mayClaim: ['creditScore'], + additionalSubjectProperties: { + body: 'Issuer of Credit Scores', + }, + }); + + // Authorized score credential + const authorizedScore = { + '@context': CREDIT_SCORE_CONTEXT, + id: 'urn:cred:score-alice', + type: ['VerifiableCredential', 'CreditScoreCredential'], + issuer: 'did:dock:b', + previousCredentialId: 'urn:cred:deleg-a-b', + rootCredentialId: 'urn:cred:deleg-a-b', + credentialSubject: { + id: 'did:example:alice', + creditScore: 760, + }, + }; + + // Unauthorized delegation credential (no creditScore claim) + const unauthorizedDelegation = createDelegationCredential({ + id: 'urn:cred:deleg-a-b', + context: CREDIT_DELEGATION_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], + issuer: 'did:dock:a', + subjectId: 'did:dock:b', + mayClaim: ['noClaim'], + }); + + it('should verify authorized credit score delegation', async () => { + const proof = createEd25519Proof({ + verificationMethod: 'did:dock:b#auth-key', + challenge: 'credit-score-example', + domain: 'docklabs.example', + created: '2025-01-17T12:15:51Z', + jws: 'test..signature', + }); + + const vp = createDelegatablePresentation( + [authorizedDelegation, authorizedScore], + proof, + PRESENTATION_CONTEXT + ); + + const result = await verifyDelegatablePresentation(vp, { policies }); + + expect(result.decision).toBe('allow'); + expect(result.failures).toStrictEqual([]); + }); + + it('should deny unauthorized credit score delegation', async () => { + const proof = createEd25519Proof({ + verificationMethod: 'did:dock:d#auth-key', + challenge: 'credit-score-example', + domain: 'docklabs.example', + created: '2025-01-17T12:15:51Z', + jws: 'test..signature', + }); + + const vp = createDelegatablePresentation( + [unauthorizedDelegation, authorizedScore], + proof, + PRESENTATION_CONTEXT + ); + + const result = await verifyDelegatablePresentation(vp, { policies }); + + expect(result.decision).not.toBe('allow'); + }); + + it('should verify delegation without Cedar policies', async () => { + const proof = createEd25519Proof({ + verificationMethod: 'did:dock:b#auth-key', + challenge: 'credit-score-example', + domain: 'docklabs.example', + created: '2025-01-17T12:15:51Z', + jws: 'test..signature', + }); + + const vp = createDelegatablePresentation( + [authorizedDelegation, authorizedScore], + proof, + PRESENTATION_CONTEXT + ); + + const result = await verifyDelegatablePresentation(vp); + + expect(result.failures).toStrictEqual([]); + }); + + it('should create delegation credentials with helper function', () => { + const delegation = createDelegationCredential({ + id: 'urn:cred:test-delegation', + issuer: 'did:dock:issuer', + subjectId: 'did:dock:subject', + mayClaim: ['claim1', 'claim2'], + additionalSubjectProperties: { + customProp: 'value', + }, + }); + + expect(delegation.id).toBe('urn:cred:test-delegation'); + expect(delegation.issuer).toBe('did:dock:issuer'); + expect(delegation.credentialSubject.id).toBe('did:dock:subject'); + expect(delegation.credentialSubject['https://rdf.dock.io/alpha/2021#mayClaim']).toEqual(['claim1', 'claim2']); + expect(delegation.credentialSubject.customProp).toBe('value'); + expect(delegation.rootCredentialId).toBe('urn:cred:test-delegation'); + }); + + it('should create Cedar policies with helper function', () => { + const policy = createCedarPolicy({ + maxDepth: 3, + rootIssuer: 'did:dock:root', + requiredClaims: { + level: 5, + role: 'admin', + }, + }); + + expect(policy.staticPolicies).toContain('context.tailDepth <= 3'); + expect(policy.staticPolicies).toContain('did:dock:root'); + expect(policy.staticPolicies).toContain('context.authorizedClaims.level >= 5'); + expect(policy.staticPolicies).toContain('context.authorizedClaims.role == "admin"'); + }); +}); diff --git a/jest.config.e2e.js b/jest.config.e2e.js index 1e16d8df..624a4833 100644 --- a/jest.config.e2e.js +++ b/jest.config.e2e.js @@ -40,9 +40,8 @@ module.exports = { '@digitalbazaar/ed25519-verification-key-2018/src/Ed25519VerificationKey2018', '@digitalbazaar/minimal-cipher': '@digitalbazaar/minimal-cipher/Cipher', '@digitalbazaar/did-method-key': '@digitalbazaar/did-method-key/lib/main', - '@digitalbazaar/http-client': require.resolve( - '@digitalbazaar/http-client/main.js', - ), + '@digitalbazaar/http-client': + '/node_modules/@digitalbazaar/http-client/dist/cjs/index.cjs', '@docknetwork/wallet-sdk-wasm/lib/(.*)': '@docknetwork/wallet-sdk-wasm/src/$1', '@docknetwork/wallet-sdk-data-store/lib/(.*)': diff --git a/packages/wasm/package.json b/packages/wasm/package.json index e351ccb3..d31d90ea 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -22,6 +22,9 @@ "@astronautlabs/jsonpath": "^1.1.2", "@docknetwork/universal-wallet": "^2.0.1", "@docknetwork/wallet-sdk-dids": "^1.7.0", + "@cedar-policy/cedar-wasm": "^4.5.0", + "jsonld": "^6.0.0", + "@docknetwork/vc-delegation-engine": "1.0.3", "@cosmjs/proto-signing": "^0.32.4", "@docknetwork/cheqd-blockchain-api": "4.0.7", "@docknetwork/cheqd-blockchain-modules": "4.0.8", diff --git a/packages/wasm/src/services/credential/delegatable-credentials.ts b/packages/wasm/src/services/credential/delegatable-credentials.ts new file mode 100644 index 00000000..87a73635 --- /dev/null +++ b/packages/wasm/src/services/credential/delegatable-credentials.ts @@ -0,0 +1,375 @@ +// @ts-nocheck +import jsonld from 'jsonld'; +import * as cedar from '@cedar-policy/cedar-wasm/nodejs'; +import { + verifyVPWithDelegation, + authorizeEvaluationsWithCedar, +} from '@docknetwork/vc-delegation-engine'; + +export interface DocumentLoaderResult { + contextUrl: string | null; + documentUrl: string; + document: any; +} + +export interface VerificationResult { + decision: string; + failures?: any[]; + evaluations?: any[]; + authorizations?: any[]; +} + +export interface CedarPolicies { + staticPolicies: string; +} + +export interface VerifiablePresentation { + '@context': any[]; + type: string[]; + proof?: any; + verifiableCredential?: any[]; +} + +export interface DelegationCredential { + '@context': any[]; + id: string; + type: string[]; + issuer: string; + previousCredentialId: string | null; + rootCredentialId: string; + credentialSubject: { + id: string; + [key: string]: any; + }; +} + +/** + * Default document loader that fetches JSON-LD contexts from URLs + * Falls back to minimal context structure for unavailable URLs + */ +export async function defaultDocumentLoader( + url: string +): Promise { + const urlString = url.toString(); + + // Try to fetch from the web first + if (urlString.startsWith('http://') || urlString.startsWith('https://')) { + try { + const response = await fetch(urlString); + if (response.ok) { + const document = await response.json(); + return { + contextUrl: null, + documentUrl: urlString, + document, + }; + } + } catch (error) { + // Fall through to return empty context + } + } + + // Return minimal context structure for known URLs + return { + contextUrl: null, + documentUrl: urlString, + document: { + '@context': { + '@version': 1.1, + }, + }, + }; +} + +/** + * Verifies a verifiable presentation with delegation chain validation + * @param vp - The verifiable presentation to verify + * @param options - Optional configuration + * @param options.documentLoader - Custom document loader function + * @param options.policies - Cedar policies for authorization + * @returns Verification result with decision and any failures + */ +export async function verifyDelegatablePresentation( + vp: VerifiablePresentation, + options: { + documentLoader?: (url: string) => Promise; + policies?: CedarPolicies; + } = {} +): Promise { + const documentLoader = options.documentLoader || defaultDocumentLoader; + + const expandedPresentation = await jsonld.expand(vp, { documentLoader }); + const credentialContexts = new Map(); + + (vp.verifiableCredential ?? []).forEach((vc: any) => { + if (vc && typeof vc.id === 'string' && vc['@context']) { + credentialContexts.set(vc.id, vc['@context']); + } + }); + + const result = await verifyVPWithDelegation({ + expandedPresentation, + credentialContexts, + documentLoader, + }); + + if (result.failures && result.failures.length > 0) { + return { ...result, decision: 'deny' }; + } + + let decision = result.decision; + let authorizations: any[] = []; + + if (options.policies) { + const authorizationOutcome = authorizeEvaluationsWithCedar({ + cedar, + evaluations: result.evaluations, + policies: options.policies, + }); + decision = authorizationOutcome.decision; + authorizations = authorizationOutcome.authorizations; + } + + return { ...result, decision, authorizations }; +} + +/** + * Creates a Cedar policy for delegation verification + * @param config - Policy configuration + * @returns Cedar policy object + */ +export function createCedarPolicy(config: { + maxDepth?: number; + rootIssuer: string; + requiredClaims?: Record; +}): CedarPolicies { + const { maxDepth = 2, rootIssuer, requiredClaims = {} } = config; + + let claimsConditions = ''; + for (const [key, value] of Object.entries(requiredClaims)) { + if (typeof value === 'number') { + claimsConditions += ` &&\n context.authorizedClaims.${key} >= ${value}`; + } else if (typeof value === 'string') { + claimsConditions += ` &&\n context.authorizedClaims.${key} == "${value}"`; + } + } + + const policyText = ` +permit( + principal in Credential::Chain::"Action:Verify", + action == Credential::Action::"Verify", + resource +) when { + principal == context.vpSigner && + context.tailDepth <= ${maxDepth} && + context.rootIssuer == Credential::Actor::"${rootIssuer}"${claimsConditions} +}; +`; + + return { staticPolicies: policyText }; +} + +/** + * Creates a verifiable presentation for delegation + * @param credentials - Array of credentials to include + * @param proof - Proof object for the presentation + * @param context - Optional additional context + * @returns Verifiable presentation object + */ +export function createDelegatablePresentation( + credentials: any[], + proof: any, + context: any[] = ['https://www.w3.org/2018/credentials/v1'] +): VerifiablePresentation { + return { + '@context': context, + type: ['VerifiablePresentation'], + proof, + verifiableCredential: credentials, + }; +} + +/** + * Delegation namespace for credential chain properties + */ +const DELEGATION_NAMESPACE = 'https://ld.truvera.io/credentials/delegation#'; + +/** + * Pre-defined context for credit delegation credentials + */ +export const CREDIT_DELEGATION_CONTEXT = [ + 'https://www.w3.org/2018/credentials/v1', + 'https://ld.truvera.io/credentials/delegation', + { + '@version': 1.1, + ex: 'https://example.org/credentials#', + delegation: DELEGATION_NAMESPACE, + CreditScoreDelegation: 'ex:CreditScoreDelegation', + DelegationCredential: 'delegation:DelegationCredential', + body: 'ex:body', + rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, + previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, + }, +]; + +/** + * Pre-defined context for credit score credentials + */ +export const CREDIT_SCORE_CONTEXT = [ + 'https://www.w3.org/2018/credentials/v1', + 'https://ld.truvera.io/credentials/delegation', + { + '@version': 1.1, + ex: 'https://example.org/credentials#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + delegation: DELEGATION_NAMESPACE, + CreditScoreCredential: 'ex:CreditScoreCredential', + creditScore: { '@id': 'ex:creditScore', '@type': 'xsd:integer' }, + rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, + previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, + }, +]; + +/** + * Pre-defined context for verifiable presentations + */ +export const PRESENTATION_CONTEXT = ['https://www.w3.org/2018/credentials/v1']; + +/** + * Creates a delegation credential + * @param params - Delegation credential parameters + * @returns Delegation credential object + */ +export function createDelegationCredential(params: { + id: string; + context?: any[]; + types?: string[]; + issuer: string; + subjectId: string; + mayClaim: string[]; + additionalSubjectProperties?: Record; + previousCredentialId?: string | null; + rootCredentialId?: string; +}): DelegationCredential { + const { + id, + context = CREDIT_DELEGATION_CONTEXT, + types = ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], + issuer, + subjectId, + mayClaim, + additionalSubjectProperties = {}, + previousCredentialId = null, + rootCredentialId, + } = params; + + return { + '@context': context, + id, + type: types, + issuer, + previousCredentialId, + rootCredentialId: rootCredentialId || id, + credentialSubject: { + id: subjectId, + 'https://rdf.dock.io/alpha/2021#mayClaim': mayClaim, + ...additionalSubjectProperties, + }, + }; +} + +/** + * Creates an Ed25519 proof object for presentations + * @param params - Proof parameters + * @returns Proof object + */ +export function createEd25519Proof(params: { + verificationMethod: string; + challenge: string; + domain: string; + created?: string; + jws?: string; +}): any { + return { + type: 'Ed25519Signature2018', + created: params.created || new Date().toISOString(), + verificationMethod: params.verificationMethod, + proofPurpose: 'authentication', + challenge: params.challenge, + domain: params.domain, + jws: params.jws || 'placeholder..signature', + }; +} + +/** + * Service class for delegatable credentials operations + */ +class DelegatableCredentialsService { + name = 'delegatable-credentials'; + + rpcMethods = [ + DelegatableCredentialsService.prototype.verifyPresentation, + DelegatableCredentialsService.prototype.createPolicy, + DelegatableCredentialsService.prototype.createPresentation, + DelegatableCredentialsService.prototype.createDelegation, + ]; + + /** + * Verifies a verifiable presentation with delegation chain + */ + async verifyPresentation(params: { + presentation: VerifiablePresentation; + policies?: CedarPolicies; + documentLoader?: (url: string) => Promise; + }): Promise { + return verifyDelegatablePresentation(params.presentation, { + policies: params.policies, + documentLoader: params.documentLoader, + }); + } + + /** + * Creates a Cedar policy for delegation verification + */ + createPolicy(params: { + maxDepth?: number; + rootIssuer: string; + requiredClaims?: Record; + }): CedarPolicies { + return createCedarPolicy(params); + } + + /** + * Creates a verifiable presentation for delegation + */ + createPresentation(params: { + credentials: any[]; + proof: any; + context?: any[]; + }): VerifiablePresentation { + return createDelegatablePresentation( + params.credentials, + params.proof, + params.context + ); + } + + /** + * Creates a delegation credential + */ + createDelegation(params: { + id: string; + context?: any[]; + types?: string[]; + issuer: string; + subjectId: string; + mayClaim: string[]; + additionalSubjectProperties?: Record; + previousCredentialId?: string | null; + rootCredentialId?: string; + }): DelegationCredential { + return createDelegationCredential(params); + } +} + +export const delegatableCredentialsService = new DelegatableCredentialsService(); From 908ca0c7fbe184ba0e698d50a96ac01f9b384494 Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 23 Dec 2025 15:15:45 -0300 Subject: [PATCH 2/7] testing delegatable credentials --- .../delegatable-credentials.test.ts | 298 +++++---- package-lock.json | 426 +++++-------- .../credential/delegatable-credentials.ts | 587 +++++++++++------- .../wasm/src/services/credential/index.ts | 17 + 4 files changed, 662 insertions(+), 666 deletions(-) diff --git a/integration-tests/delegatable-credentials.test.ts b/integration-tests/delegatable-credentials.test.ts index b78fa4ee..d81c5f91 100644 --- a/integration-tests/delegatable-credentials.test.ts +++ b/integration-tests/delegatable-credentials.test.ts @@ -1,190 +1,180 @@ import { verifyDelegatablePresentation, + issueDelegationCredential, + issueDelegatedCredential, + createSignedPresentation, createCedarPolicy, - createDelegatablePresentation, - createDelegationCredential, - createEd25519Proof, + MAY_CLAIM_IRI, } from '@docknetwork/wallet-sdk-wasm/src/services/credential/delegatable-credentials'; +import { didService } from '@docknetwork/wallet-sdk-wasm/src/services/dids/service'; +// ============================================================================ +// TEST CONFIGURATION +// ============================================================================ -/** - * Delegation namespace for credential chain properties - */ -const DELEGATION_NAMESPACE = 'https://ld.truvera.io/credentials/delegation#'; - -/** - * Pre-defined context for credit delegation credentials - */ -const CREDIT_DELEGATION_CONTEXT = [ - 'https://www.w3.org/2018/credentials/v1', - 'https://ld.truvera.io/credentials/delegation', - { - '@version': 1.1, - ex: 'https://example.org/credentials#', - delegation: DELEGATION_NAMESPACE, - CreditScoreDelegation: 'ex:CreditScoreDelegation', - DelegationCredential: 'delegation:DelegationCredential', - body: 'ex:body', - rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, - previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, - }, -]; - -/** - * Pre-defined context for credit score credentials - */ -const CREDIT_SCORE_CONTEXT = [ - 'https://www.w3.org/2018/credentials/v1', - 'https://ld.truvera.io/credentials/delegation', - { - '@version': 1.1, - ex: 'https://example.org/credentials#', - xsd: 'http://www.w3.org/2001/XMLSchema#', - delegation: DELEGATION_NAMESPACE, - CreditScoreCredential: 'ex:CreditScoreCredential', - creditScore: { '@id': 'ex:creditScore', '@type': 'xsd:integer' }, - rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, - previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, - }, -]; - -/** - * Pre-defined context for verifiable presentations - */ -const PRESENTATION_CONTEXT = ['https://www.w3.org/2018/credentials/v1']; +const DELEGATION_ROOT_ID = 'urn:cred:delegation-root'; +const CREDIT_SCORE_CRED_ID = 'urn:cred:credit-score-alice'; +const SUBJECT_DID = 'did:example:alice'; + +const CHALLENGE = 'test-challenge-123'; +const DOMAIN = 'test.example.com'; describe('Delegatable Credentials', () => { - // Cedar policy for credit score delegation - const policies = createCedarPolicy({ - maxDepth: 2, - rootIssuer: 'did:dock:a', - requiredClaims: { - creditScore: 0, - body: 'Issuer of Credit Scores', - }, - }); + let rootIssuerKey: any; + let delegateKey: any; + let rootIssuerDid: string; + let delegateDid: string; + let delegationCredential: any; + let creditScoreCredential: any; + let unauthorizedDelegationCredential: any; + + beforeAll(async () => { + // Generate key pairs for root issuer and delegate + rootIssuerKey = await didService.generateKeyDoc({ + type: 'ed25519', + }); + delegateKey = await didService.generateKeyDoc({ + type: 'ed25519', + }); - // Authorized delegation credential - const authorizedDelegation = createDelegationCredential({ - id: 'urn:cred:deleg-a-b', - context: CREDIT_DELEGATION_CONTEXT, - types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], - issuer: 'did:dock:a', - subjectId: 'did:dock:b', - mayClaim: ['creditScore'], - additionalSubjectProperties: { - body: 'Issuer of Credit Scores', - }, - }); + // Extract DIDs from the key documents + rootIssuerDid = rootIssuerKey.controller; + delegateDid = delegateKey.controller; - // Authorized score credential - const authorizedScore = { - '@context': CREDIT_SCORE_CONTEXT, - id: 'urn:cred:score-alice', - type: ['VerifiableCredential', 'CreditScoreCredential'], - issuer: 'did:dock:b', - previousCredentialId: 'urn:cred:deleg-a-b', - rootCredentialId: 'urn:cred:deleg-a-b', - credentialSubject: { - id: 'did:example:alice', - creditScore: 760, - }, - }; - - // Unauthorized delegation credential (no creditScore claim) - const unauthorizedDelegation = createDelegationCredential({ - id: 'urn:cred:deleg-a-b', - context: CREDIT_DELEGATION_CONTEXT, - types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], - issuer: 'did:dock:a', - subjectId: 'did:dock:b', - mayClaim: ['noClaim'], - }); + console.log('Root Issuer DID:', rootIssuerDid); + console.log('Delegate DID:', delegateDid); - it('should verify authorized credit score delegation', async () => { - const proof = createEd25519Proof({ - verificationMethod: 'did:dock:b#auth-key', - challenge: 'credit-score-example', - domain: 'docklabs.example', - created: '2025-01-17T12:15:51Z', - jws: 'test..signature', + // Issue the root delegation credential + // This grants the delegate authority to issue creditScore claims + delegationCredential = await issueDelegationCredential(rootIssuerKey, { + id: DELEGATION_ROOT_ID, + issuerDid: rootIssuerDid, + delegateDid: delegateDid, + mayClaim: ['creditScore'], + additionalSubjectProperties: { + body: 'Issuer of Credit Scores', + }, + }); + + // Issue a credit score credential as the delegate + creditScoreCredential = await issueDelegatedCredential(delegateKey, { + id: CREDIT_SCORE_CRED_ID, + issuerDid: delegateDid, + subjectDid: SUBJECT_DID, + claims: { + creditScore: 760, + }, + rootCredentialId: DELEGATION_ROOT_ID, + previousCredentialId: DELEGATION_ROOT_ID, }); - const vp = createDelegatablePresentation( - [authorizedDelegation, authorizedScore], - proof, - PRESENTATION_CONTEXT - ); + // Issue an unauthorized delegation (no creditScore in mayClaim) + unauthorizedDelegationCredential = await issueDelegationCredential(rootIssuerKey, { + id: 'urn:cred:unauthorized-delegation', + issuerDid: rootIssuerDid, + delegateDid: delegateDid, + mayClaim: ['someOtherClaim'], // Does NOT include creditScore + }); + }); - const result = await verifyDelegatablePresentation(vp, { policies }); + it('should issue a valid delegation credential', () => { + expect(delegationCredential).toBeDefined(); + expect(delegationCredential.id).toBe(DELEGATION_ROOT_ID); + expect(delegationCredential.issuer).toBe(rootIssuerDid); + expect(delegationCredential.credentialSubject.id).toBe(delegateDid); + expect(delegationCredential.credentialSubject[MAY_CLAIM_IRI]).toContain('creditScore'); + expect(delegationCredential.proof).toBeDefined(); + expect(delegationCredential.rootCredentialId).toBe(DELEGATION_ROOT_ID); + expect(delegationCredential.previousCredentialId).toBeNull(); + }); - expect(result.decision).toBe('allow'); - expect(result.failures).toStrictEqual([]); + it('should issue a valid delegated credential', () => { + expect(creditScoreCredential).toBeDefined(); + expect(creditScoreCredential.id).toBe(CREDIT_SCORE_CRED_ID); + expect(creditScoreCredential.issuer).toBe(delegateDid); + expect(creditScoreCredential.credentialSubject.id).toBe(SUBJECT_DID); + expect(creditScoreCredential.credentialSubject.creditScore).toBe(760); + expect(creditScoreCredential.proof).toBeDefined(); + expect(creditScoreCredential.rootCredentialId).toBe(DELEGATION_ROOT_ID); + expect(creditScoreCredential.previousCredentialId).toBe(DELEGATION_ROOT_ID); }); - it('should deny unauthorized credit score delegation', async () => { - const proof = createEd25519Proof({ - verificationMethod: 'did:dock:d#auth-key', - challenge: 'credit-score-example', - domain: 'docklabs.example', - created: '2025-01-17T12:15:51Z', - jws: 'test..signature', + it('should verify authorized delegation with Cedar policies', async () => { + // Create a signed presentation with both credentials + const presentation = await createSignedPresentation(delegateKey, { + credentials: [delegationCredential, creditScoreCredential], + holderDid: delegateDid, + challenge: CHALLENGE, + domain: DOMAIN, }); - const vp = createDelegatablePresentation( - [unauthorizedDelegation, authorizedScore], - proof, - PRESENTATION_CONTEXT - ); + // Create Cedar policy that allows this delegation + const policies = createCedarPolicy({ + maxDepth: 2, + rootIssuer: rootIssuerDid, + requiredClaims: { + creditScore: 0, + body: 'Issuer of Credit Scores', + }, + }); - const result = await verifyDelegatablePresentation(vp, { policies }); + // Verify the presentation + const result = await verifyDelegatablePresentation(presentation, { + challenge: CHALLENGE, + domain: DOMAIN, + policies, + }); + + console.log('Verification result:', { + verified: result.verified, + delegationDecision: result.delegationResult?.decision, + credentialResults: result.credentialResults?.map(r => r.verified), + }); - expect(result.decision).not.toBe('allow'); + // Check delegation result + expect(result.delegationResult).toBeDefined(); + expect(result.delegationResult?.decision).toBe('allow'); + + // Log delegation summary for debugging + if (result.delegationResult?.summaries?.length > 0) { + const summary = result.delegationResult.summaries[0]; + console.log('Delegation summary:', { + rootIssuer: summary.rootIssuer, + tailIssuer: summary.tailIssuer, + tailDepth: summary.tailDepth, + authorizedClaims: summary.authorizedClaims, + }); + } }); it('should verify delegation without Cedar policies', async () => { - const proof = createEd25519Proof({ - verificationMethod: 'did:dock:b#auth-key', - challenge: 'credit-score-example', - domain: 'docklabs.example', - created: '2025-01-17T12:15:51Z', - jws: 'test..signature', + // Create a signed presentation + const presentation = await createSignedPresentation(delegateKey, { + credentials: [delegationCredential, creditScoreCredential], + holderDid: delegateDid, + challenge: CHALLENGE, + domain: DOMAIN, }); - const vp = createDelegatablePresentation( - [authorizedDelegation, authorizedScore], - proof, - PRESENTATION_CONTEXT - ); - - const result = await verifyDelegatablePresentation(vp); - - expect(result.failures).toStrictEqual([]); - }); + // Verify without Cedar policies - just validates delegation chain + const result = await verifyDelegatablePresentation(presentation, { + challenge: CHALLENGE, + domain: DOMAIN, + }); - it('should create delegation credentials with helper function', () => { - const delegation = createDelegationCredential({ - id: 'urn:cred:test-delegation', - issuer: 'did:dock:issuer', - subjectId: 'did:dock:subject', - mayClaim: ['claim1', 'claim2'], - additionalSubjectProperties: { - customProp: 'value', - }, + console.log('Verification without policies:', { + verified: result.verified, + delegationDecision: result.delegationResult?.decision, }); - expect(delegation.id).toBe('urn:cred:test-delegation'); - expect(delegation.issuer).toBe('did:dock:issuer'); - expect(delegation.credentialSubject.id).toBe('did:dock:subject'); - expect(delegation.credentialSubject['https://rdf.dock.io/alpha/2021#mayClaim']).toEqual(['claim1', 'claim2']); - expect(delegation.credentialSubject.customProp).toBe('value'); - expect(delegation.rootCredentialId).toBe('urn:cred:test-delegation'); + expect(result.delegationResult).toBeDefined(); + expect(result.delegationResult?.failures || []).toHaveLength(0); }); it('should create Cedar policies with helper function', () => { const policy = createCedarPolicy({ maxDepth: 3, - rootIssuer: 'did:dock:root', + rootIssuer: 'did:example:root', requiredClaims: { level: 5, role: 'admin', @@ -192,7 +182,7 @@ describe('Delegatable Credentials', () => { }); expect(policy.staticPolicies).toContain('context.tailDepth <= 3'); - expect(policy.staticPolicies).toContain('did:dock:root'); + expect(policy.staticPolicies).toContain('did:example:root'); expect(policy.staticPolicies).toContain('context.authorizedClaims.level >= 5'); expect(policy.staticPolicies).toContain('context.authorizedClaims.role == "admin"'); }); diff --git a/package-lock.json b/package-lock.json index cd8afd4b..66b1de9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2374,6 +2374,11 @@ "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.7.0.tgz", "integrity": "sha512-qn6tAIZEw5i/wiESBF4nQxZkl86aY4KoO0IkUa2Lh+rya64oTOdJQFlZuMwI1Qz9VBJQrQC4QlSA2DNek5gCOA==" }, + "node_modules/@cedar-policy/cedar-wasm": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@cedar-policy/cedar-wasm/-/cedar-wasm-4.8.2.tgz", + "integrity": "sha512-S37Kd4wP/IMZN3pdKEcsV8av7jMj4AKRovxzJEYZNTEYq0Wj4fno3dsw8xHHDXqT0dkQGTNUBuQNF8CTvOgE/Q==" + }, "node_modules/@cheqd/sdk": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/@cheqd/sdk/-/sdk-5.3.2.tgz", @@ -3473,17 +3478,76 @@ } }, "node_modules/@digitalbazaar/http-client": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-1.2.0.tgz", - "integrity": "sha512-W9KQQ5pUJcaR0I4c2HPJC0a7kRbZApIorZgPnEDwMBgj16iQzutGLrCXYaZOmxqVLVNqqlQ4aUJh+HBQZy4W6Q==", - "license": "BSD-3-Clause", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", + "integrity": "sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==", "dependencies": { - "esm": "^3.2.22", - "ky": "^0.25.1", - "ky-universal": "^0.8.2" + "ky": "^0.33.3", + "ky-universal": "^0.11.0", + "undici": "^5.21.2" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.0" + } + }, + "node_modules/@digitalbazaar/http-client/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@digitalbazaar/http-client/node_modules/ky": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", + "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/@digitalbazaar/http-client/node_modules/ky-universal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.11.0.tgz", + "integrity": "sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^3.2.10" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky-universal?sponsor=1" + }, + "peerDependencies": { + "ky": ">=0.31.4", + "web-streams-polyfill": ">=3.2.1" + }, + "peerDependenciesMeta": { + "web-streams-polyfill": { + "optional": true + } + } + }, + "node_modules/@digitalbazaar/http-client/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/@digitalbazaar/http-digest-header": { @@ -3849,91 +3913,18 @@ "node": ">=22.0.0" } }, - "node_modules/@docknetwork/credential-sdk/node_modules/@digitalbazaar/http-client": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", - "integrity": "sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==", - "dependencies": { - "ky": "^0.33.3", - "ky-universal": "^0.11.0", - "undici": "^5.21.2" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/@docknetwork/credential-sdk/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@docknetwork/credential-sdk/node_modules/jsonld": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-6.0.0.tgz", - "integrity": "sha512-1SkN2RXhMCTCSkX+bzHvr9ycM2HTmjWyV41hn2xG7k6BqlCgRjw0zHmuqfphjBRPqi1gKMIqgBCe/0RZMcWrAA==", - "dependencies": { - "@digitalbazaar/http-client": "^3.2.0", - "canonicalize": "^1.0.1", - "lru-cache": "^6.0.0", - "rdf-canonize": "^3.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@docknetwork/credential-sdk/node_modules/ky": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", - "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky?sponsor=1" - } - }, - "node_modules/@docknetwork/credential-sdk/node_modules/ky-universal": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.11.0.tgz", - "integrity": "sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==", + "node_modules/@docknetwork/credential-sdk/node_modules/@docknetwork/vc-delegation-engine": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@docknetwork/vc-delegation-engine/-/vc-delegation-engine-1.0.2.tgz", + "integrity": "sha512-AlFoQrvDDJ7TpfbDY9e44XfhOHmBcvSLpgj79NqdzcWKiCRqE+zIKff9Rc3WXpUmGuhpr6uJjULcro8xMW+qZQ==", "dependencies": { - "abort-controller": "^3.0.0", - "node-fetch": "^3.2.10" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky-universal?sponsor=1" + "base64url": "^3.0.1", + "jsonld": "^6.0.0", + "jsonpath-plus": "^10.1.0", + "rify": "^0.7.1" }, "peerDependencies": { - "ky": ">=0.31.4", - "web-streams-polyfill": ">=3.2.1" - }, - "peerDependenciesMeta": { - "web-streams-polyfill": { - "optional": true - } - } - }, - "node_modules/@docknetwork/credential-sdk/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "@cedar-policy/cedar-wasm": "^4.5.0" } }, "node_modules/@docknetwork/credential-sdk/node_modules/semver": { @@ -4036,9 +4027,9 @@ } }, "node_modules/@docknetwork/vc-delegation-engine": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@docknetwork/vc-delegation-engine/-/vc-delegation-engine-1.0.2.tgz", - "integrity": "sha512-AlFoQrvDDJ7TpfbDY9e44XfhOHmBcvSLpgj79NqdzcWKiCRqE+zIKff9Rc3WXpUmGuhpr6uJjULcro8xMW+qZQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@docknetwork/vc-delegation-engine/-/vc-delegation-engine-1.0.3.tgz", + "integrity": "sha512-cOSHvPd5z/UlYyYj5P+WMIEqTnZNU0Wv/sXrmg+45K8P4uq7gNgBwMvVpoOjGe/KonFheMSBhFHd51MXIeMl5w==", "dependencies": { "base64url": "^3.0.1", "jsonld": "^6.0.0", @@ -4049,93 +4040,6 @@ "@cedar-policy/cedar-wasm": "^4.5.0" } }, - "node_modules/@docknetwork/vc-delegation-engine/node_modules/@digitalbazaar/http-client": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", - "integrity": "sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==", - "dependencies": { - "ky": "^0.33.3", - "ky-universal": "^0.11.0", - "undici": "^5.21.2" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/@docknetwork/vc-delegation-engine/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@docknetwork/vc-delegation-engine/node_modules/jsonld": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-6.0.0.tgz", - "integrity": "sha512-1SkN2RXhMCTCSkX+bzHvr9ycM2HTmjWyV41hn2xG7k6BqlCgRjw0zHmuqfphjBRPqi1gKMIqgBCe/0RZMcWrAA==", - "dependencies": { - "@digitalbazaar/http-client": "^3.2.0", - "canonicalize": "^1.0.1", - "lru-cache": "^6.0.0", - "rdf-canonize": "^3.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@docknetwork/vc-delegation-engine/node_modules/ky": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", - "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky?sponsor=1" - } - }, - "node_modules/@docknetwork/vc-delegation-engine/node_modules/ky-universal": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.11.0.tgz", - "integrity": "sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==", - "dependencies": { - "abort-controller": "^3.0.0", - "node-fetch": "^3.2.10" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky-universal?sponsor=1" - }, - "peerDependencies": { - "ky": ">=0.31.4", - "web-streams-polyfill": ">=3.2.1" - }, - "peerDependenciesMeta": { - "web-streams-polyfill": { - "optional": true - } - } - }, - "node_modules/@docknetwork/vc-delegation-engine/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/@docknetwork/wallet-edv-storage": { "resolved": "packages/wallet-edv-storage", "link": true @@ -7656,6 +7560,33 @@ "node": ">=16" } }, + "node_modules/@transmute/jsonld/node_modules/@digitalbazaar/http-client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-1.2.0.tgz", + "integrity": "sha512-W9KQQ5pUJcaR0I4c2HPJC0a7kRbZApIorZgPnEDwMBgj16iQzutGLrCXYaZOmxqVLVNqqlQ4aUJh+HBQZy4W6Q==", + "dependencies": { + "esm": "^3.2.22", + "ky": "^0.25.1", + "ky-universal": "^0.8.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@transmute/jsonld/node_modules/jsonld": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-5.2.0.tgz", + "integrity": "sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw==", + "dependencies": { + "@digitalbazaar/http-client": "^1.1.0", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@transmute/ld-key-pair": { "version": "0.7.0-unstable.82", "resolved": "https://registry.npmjs.org/@transmute/ld-key-pair/-/ld-key-pair-0.7.0-unstable.82.tgz", @@ -17279,7 +17210,6 @@ "url": "https://paypal.me/jimmywarting" } ], - "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -17288,13 +17218,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/fetch-blob/node_modules/web-streams-polyfill": { - "version": "3.2.1", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -17690,7 +17613,6 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" }, @@ -24878,18 +24800,17 @@ } }, "node_modules/jsonld": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-5.2.0.tgz", - "integrity": "sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw==", - "license": "BSD-3-Clause", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-6.0.0.tgz", + "integrity": "sha512-1SkN2RXhMCTCSkX+bzHvr9ycM2HTmjWyV41hn2xG7k6BqlCgRjw0zHmuqfphjBRPqi1gKMIqgBCe/0RZMcWrAA==", "dependencies": { - "@digitalbazaar/http-client": "^1.1.0", + "@digitalbazaar/http-client": "^3.2.0", "canonicalize": "^1.0.1", "lru-cache": "^6.0.0", "rdf-canonize": "^3.0.0" }, "engines": { - "node": ">=12" + "node": ">=14" } }, "node_modules/jsonld-document-loader": { @@ -24915,6 +24836,33 @@ "node": ">=12" } }, + "node_modules/jsonld-signatures/node_modules/@digitalbazaar/http-client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-1.2.0.tgz", + "integrity": "sha512-W9KQQ5pUJcaR0I4c2HPJC0a7kRbZApIorZgPnEDwMBgj16iQzutGLrCXYaZOmxqVLVNqqlQ4aUJh+HBQZy4W6Q==", + "dependencies": { + "esm": "^3.2.22", + "ky": "^0.25.1", + "ky-universal": "^0.8.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/jsonld-signatures/node_modules/jsonld": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-5.2.0.tgz", + "integrity": "sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw==", + "dependencies": { + "@digitalbazaar/http-client": "^1.1.0", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -26907,6 +26855,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -26917,7 +26866,6 @@ "url": "https://paypal.me/jimmywarting" } ], - "license": "MIT", "engines": { "node": ">=10.5.0" } @@ -36298,21 +36246,6 @@ "node": ">=14" } }, - "packages/wallet-edv-storage/node_modules/@digitalbazaar/http-client": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", - "integrity": "sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "ky": "^0.33.3", - "ky-universal": "^0.11.0", - "undici": "^5.21.2" - }, - "engines": { - "node": ">=14.0" - } - }, "packages/wallet-edv-storage/node_modules/@digitalbazaar/http-digest-header": { "version": "2.0.0", "license": "BSD-3-Clause", @@ -36470,74 +36403,6 @@ "node": ">=14" } }, - "packages/wallet-edv-storage/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 12" - } - }, - "packages/wallet-edv-storage/node_modules/ky": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", - "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky?sponsor=1" - } - }, - "packages/wallet-edv-storage/node_modules/ky-universal": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.11.0.tgz", - "integrity": "sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==", - "license": "MIT", - "optional": true, - "dependencies": { - "abort-controller": "^3.0.0", - "node-fetch": "^3.2.10" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky-universal?sponsor=1" - }, - "peerDependencies": { - "ky": ">=0.31.4", - "web-streams-polyfill": ">=3.2.1" - }, - "peerDependenciesMeta": { - "web-streams-polyfill": { - "optional": true - } - } - }, - "packages/wallet-edv-storage/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "optional": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "packages/wallet-edv-storage/node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", @@ -36561,11 +36426,13 @@ "license": "https://github.com/docknetwork/wallet-sdk/LICENSE", "dependencies": { "@astronautlabs/jsonpath": "^1.1.2", + "@cedar-policy/cedar-wasm": "^4.5.0", "@cosmjs/proto-signing": "^0.32.4", "@docknetwork/cheqd-blockchain-api": "4.0.7", "@docknetwork/cheqd-blockchain-modules": "4.0.8", "@docknetwork/credential-sdk": "0.54.11", "@docknetwork/universal-wallet": "^2.0.1", + "@docknetwork/vc-delegation-engine": "1.0.3", "@docknetwork/wallet-sdk-dids": "^1.7.0", "@noble/hashes": "1.8.0", "@scure/bip39": "^1.6.0", @@ -36579,6 +36446,7 @@ "base64url": "^3.0.1", "cwait": "1.1.2", "json-rpc-2.0": "^0.2.16", + "jsonld": "^6.0.0", "p-limit": "2.3.0", "uuid": "^8.3.2", "winston": "^3.3.3" diff --git a/packages/wasm/src/services/credential/delegatable-credentials.ts b/packages/wasm/src/services/credential/delegatable-credentials.ts index 87a73635..8d6e5cce 100644 --- a/packages/wasm/src/services/credential/delegatable-credentials.ts +++ b/packages/wasm/src/services/credential/delegatable-credentials.ts @@ -1,22 +1,44 @@ // @ts-nocheck -import jsonld from 'jsonld'; import * as cedar from '@cedar-policy/cedar-wasm/nodejs'; import { - verifyVPWithDelegation, - authorizeEvaluationsWithCedar, -} from '@docknetwork/vc-delegation-engine'; - -export interface DocumentLoaderResult { - contextUrl: string | null; - documentUrl: string; - document: any; + verifyPresentation, + issueCredential, + signPresentation, + documentLoader, + getSuiteFromKeyDoc, +} from '@docknetwork/credential-sdk/vc'; +import { MAY_CLAIM_IRI } from '@docknetwork/vc-delegation-engine'; +import { getKeypairFromDoc } from '@docknetwork/universal-wallet/methods/keypairs'; +import { blockchainService } from '../blockchain/service'; + +/** + * Prepares a key document for signing by creating a proper keypair with signer capability + * @param keyDoc - The key document with id, controller, type, and key material + * @returns A key document with an active signer + */ +function prepareKeyForSigning(keyDoc: KeyPair): any { + const kp = getKeypairFromDoc(keyDoc); + // Get the signer from the keypair - this returns an object with id and sign method + const signer = kp.signer(); + // Set the id on the signer to match the verification method + signer.id = keyDoc.id; + return { + ...keyDoc, + keypair: kp, + signer, + }; } export interface VerificationResult { - decision: string; - failures?: any[]; - evaluations?: any[]; - authorizations?: any[]; + verified: boolean; + credentialResults?: any[]; + delegationResult?: { + decision: string; + summaries?: any[]; + authorizations?: any[]; + failures?: any[]; + }; + error?: any; } export interface CedarPolicies { @@ -35,102 +57,279 @@ export interface DelegationCredential { id: string; type: string[]; issuer: string; + issuanceDate: string; previousCredentialId: string | null; rootCredentialId: string; credentialSubject: { id: string; [key: string]: any; }; + proof?: any; +} + +export interface VerifyDelegationOptions { + challenge?: string; + domain?: string; + unsignedPresentation?: boolean; + failOnUnauthorizedClaims?: boolean; + policies?: CedarPolicies; +} + +export interface KeyPair { + type: string; + id?: string; + controller?: string; + publicKeyJwk?: any; + privateKeyJwk?: any; + publicKeyBase58?: string; + privateKeyBase58?: string; } /** - * Default document loader that fetches JSON-LD contexts from URLs - * Falls back to minimal context structure for unavailable URLs + * W3C Credentials V1 context URL */ -export async function defaultDocumentLoader( - url: string -): Promise { - const urlString = url.toString(); - - // Try to fetch from the web first - if (urlString.startsWith('http://') || urlString.startsWith('https://')) { - try { - const response = await fetch(urlString); - if (response.ok) { - const document = await response.json(); - return { - contextUrl: null, - documentUrl: urlString, - document, - }; - } - } catch (error) { - // Fall through to return empty context - } +export const W3C_CREDENTIALS_V1 = 'https://www.w3.org/2018/credentials/v1'; + +/** + * Delegation context URL (for documentation purposes - we embed the context inline) + */ +export const DELEGATION_CONTEXT_URL = 'https://ld.truvera.io/credentials/delegation'; + +/** + * Re-export MAY_CLAIM_IRI for use in credentials + */ +export { MAY_CLAIM_IRI }; + +/** + * Embedded delegation context terms + * This defines the JSON-LD terms needed for delegation credentials + */ +const DELEGATION_CONTEXT_TERMS = { + '@version': 1.1, + '@protected': true, + delegation: 'https://rdf.dock.io/credentials/delegation#', + DelegationCredential: 'delegation:DelegationCredential', + mayClaim: { '@id': MAY_CLAIM_IRI, '@container': '@set' }, + rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, + previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, +}; + +/** + * Pre-defined context for delegation credentials + */ +export const DELEGATION_CREDENTIAL_CONTEXT = [ + W3C_CREDENTIALS_V1, + { + ...DELEGATION_CONTEXT_TERMS, + dock: 'https://rdf.dock.io/alpha/2021#', + ex: 'https://example.org/credentials#', + CreditScoreDelegation: 'ex:CreditScoreDelegation', + body: 'ex:body', + }, +]; + +/** + * Pre-defined context for credit score credentials + */ +export const CREDIT_SCORE_CONTEXT = [ + W3C_CREDENTIALS_V1, + { + ...DELEGATION_CONTEXT_TERMS, + ex: 'https://example.org/credentials#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + CreditScoreCredential: 'ex:CreditScoreCredential', + creditScore: { '@id': 'ex:creditScore', '@type': 'xsd:integer' }, + }, +]; + +/** + * Pre-defined context for verifiable presentations + */ +export const PRESENTATION_CONTEXT = [W3C_CREDENTIALS_V1]; + +/** + * Issues a delegation credential that grants authority to a delegate + * @param keyPair - The key pair to sign the credential + * @param params - Delegation parameters + * @returns Signed delegation credential + */ +export async function issueDelegationCredential( + keyPair: KeyPair, + params: { + id: string; + issuerDid: string; + delegateDid: string; + mayClaim: string[]; + context?: any[]; + types?: string[]; + additionalSubjectProperties?: Record; + previousCredentialId?: string | null; + rootCredentialId?: string; } +): Promise { + const { + id, + issuerDid, + delegateDid, + mayClaim, + context = DELEGATION_CREDENTIAL_CONTEXT, + types = ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], + additionalSubjectProperties = {}, + previousCredentialId = null, + rootCredentialId, + } = params; - // Return minimal context structure for known URLs - return { - contextUrl: null, - documentUrl: urlString, - document: { - '@context': { - '@version': 1.1, - }, + const credential = { + '@context': context, + id, + type: types, + issuer: issuerDid, + issuanceDate: new Date().toISOString(), + credentialSubject: { + id: delegateDid, + [MAY_CLAIM_IRI]: mayClaim, + ...additionalSubjectProperties, }, + rootCredentialId: rootCredentialId || id, + previousCredentialId, }; + + const preparedKey = prepareKeyForSigning(keyPair); + return issueCredential(preparedKey, credential); } /** - * Verifies a verifiable presentation with delegation chain validation - * @param vp - The verifiable presentation to verify - * @param options - Optional configuration - * @param options.documentLoader - Custom document loader function - * @param options.policies - Cedar policies for authorization - * @returns Verification result with decision and any failures + * Issues a credential as a delegate (with delegation chain reference) + * @param keyPair - The delegate's key pair to sign the credential + * @param params - Credential parameters + * @returns Signed credential */ -export async function verifyDelegatablePresentation( - vp: VerifiablePresentation, - options: { - documentLoader?: (url: string) => Promise; - policies?: CedarPolicies; - } = {} -): Promise { - const documentLoader = options.documentLoader || defaultDocumentLoader; - - const expandedPresentation = await jsonld.expand(vp, { documentLoader }); - const credentialContexts = new Map(); +export async function issueDelegatedCredential( + keyPair: KeyPair, + params: { + id: string; + issuerDid: string; + subjectDid: string; + claims: Record; + rootCredentialId: string; + previousCredentialId: string; + context?: any[]; + types?: string[]; + } +): Promise { + const { + id, + issuerDid, + subjectDid, + claims, + rootCredentialId, + previousCredentialId, + context = CREDIT_SCORE_CONTEXT, + types = ['VerifiableCredential', 'CreditScoreCredential'], + } = params; - (vp.verifiableCredential ?? []).forEach((vc: any) => { - if (vc && typeof vc.id === 'string' && vc['@context']) { - credentialContexts.set(vc.id, vc['@context']); - } - }); + const credential = { + '@context': context, + id, + type: types, + issuer: issuerDid, + issuanceDate: new Date().toISOString(), + credentialSubject: { + id: subjectDid, + ...claims, + }, + rootCredentialId, + previousCredentialId, + }; - const result = await verifyVPWithDelegation({ - expandedPresentation, - credentialContexts, - documentLoader, - }); + const preparedKey = prepareKeyForSigning(keyPair); + return issueCredential(preparedKey, credential); +} - if (result.failures && result.failures.length > 0) { - return { ...result, decision: 'deny' }; +/** + * Creates and signs a verifiable presentation with delegation credentials + * @param keyPair - The key pair to sign the presentation + * @param params - Presentation parameters + * @returns Signed verifiable presentation + */ +export async function createSignedPresentation( + keyPair: KeyPair, + params: { + credentials: any[]; + holderDid: string; + challenge: string; + domain: string; + context?: any[]; } +): Promise { + const { + credentials, + holderDid, + challenge, + domain, + context = PRESENTATION_CONTEXT, + } = params; + + const presentation = { + '@context': context, + type: ['VerifiablePresentation'], + holder: holderDid, + verifiableCredential: credentials, + }; + + // Create key document for signing with proper keypair + const keyDoc = { + ...keyPair, + id: keyPair.id || `${holderDid}#keys-1`, + controller: keyPair.controller || holderDid, + }; + + const preparedKey = prepareKeyForSigning(keyDoc); + return signPresentation(presentation, preparedKey, challenge, domain); +} - let decision = result.decision; - let authorizations: any[] = []; +/** + * Verifies a verifiable presentation with optional delegation chain validation + * Uses the credential-sdk's verifyPresentation which automatically: + * 1. Verifies the presentation signature + * 2. Verifies all credentials + * 3. Detects delegation credentials + * 4. Validates the delegation chain + * 5. Applies Cedar policies if provided + * + * @param vp - The verifiable presentation to verify + * @param options - Verification options + * @returns Verification result with delegation info if applicable + */ +export async function verifyDelegatablePresentation( + vp: VerifiablePresentation, + options: VerifyDelegationOptions = {} +): Promise { + const { + challenge = vp.proof?.challenge || 'default-challenge', + domain = vp.proof?.domain || 'default-domain', + unsignedPresentation = false, + failOnUnauthorizedClaims = true, + policies, + } = options; + + const verifyOptions: any = { + challenge, + domain, + documentLoader: documentLoader(blockchainService.resolver), + unsignedPresentation, + failOnUnauthorizedClaims, + }; - if (options.policies) { - const authorizationOutcome = authorizeEvaluationsWithCedar({ + // Add Cedar authorization if policies are provided + if (policies) { + verifyOptions.cedarAuth = { + policies, cedar, - evaluations: result.evaluations, - policies: options.policies, - }); - decision = authorizationOutcome.decision; - authorizations = authorizationOutcome.authorizations; + }; } - return { ...result, decision, authorizations }; + return verifyPresentation(vp, verifyOptions); } /** @@ -170,136 +369,34 @@ permit( } /** - * Creates a verifiable presentation for delegation + * Creates an unsigned verifiable presentation (for testing) * @param credentials - Array of credentials to include - * @param proof - Proof object for the presentation - * @param context - Optional additional context + * @param proof - Optional proof object + * @param context - Optional context * @returns Verifiable presentation object */ -export function createDelegatablePresentation( +export function createUnsignedPresentation( credentials: any[], - proof: any, - context: any[] = ['https://www.w3.org/2018/credentials/v1'] + proof?: any, + context: any[] = PRESENTATION_CONTEXT ): VerifiablePresentation { - return { + const vp: VerifiablePresentation = { '@context': context, type: ['VerifiablePresentation'], - proof, verifiableCredential: credentials, }; -} - -/** - * Delegation namespace for credential chain properties - */ -const DELEGATION_NAMESPACE = 'https://ld.truvera.io/credentials/delegation#'; - -/** - * Pre-defined context for credit delegation credentials - */ -export const CREDIT_DELEGATION_CONTEXT = [ - 'https://www.w3.org/2018/credentials/v1', - 'https://ld.truvera.io/credentials/delegation', - { - '@version': 1.1, - ex: 'https://example.org/credentials#', - delegation: DELEGATION_NAMESPACE, - CreditScoreDelegation: 'ex:CreditScoreDelegation', - DelegationCredential: 'delegation:DelegationCredential', - body: 'ex:body', - rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, - previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, - }, -]; - -/** - * Pre-defined context for credit score credentials - */ -export const CREDIT_SCORE_CONTEXT = [ - 'https://www.w3.org/2018/credentials/v1', - 'https://ld.truvera.io/credentials/delegation', - { - '@version': 1.1, - ex: 'https://example.org/credentials#', - xsd: 'http://www.w3.org/2001/XMLSchema#', - delegation: DELEGATION_NAMESPACE, - CreditScoreCredential: 'ex:CreditScoreCredential', - creditScore: { '@id': 'ex:creditScore', '@type': 'xsd:integer' }, - rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, - previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, - }, -]; - -/** - * Pre-defined context for verifiable presentations - */ -export const PRESENTATION_CONTEXT = ['https://www.w3.org/2018/credentials/v1']; -/** - * Creates a delegation credential - * @param params - Delegation credential parameters - * @returns Delegation credential object - */ -export function createDelegationCredential(params: { - id: string; - context?: any[]; - types?: string[]; - issuer: string; - subjectId: string; - mayClaim: string[]; - additionalSubjectProperties?: Record; - previousCredentialId?: string | null; - rootCredentialId?: string; -}): DelegationCredential { - const { - id, - context = CREDIT_DELEGATION_CONTEXT, - types = ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], - issuer, - subjectId, - mayClaim, - additionalSubjectProperties = {}, - previousCredentialId = null, - rootCredentialId, - } = params; + if (proof) { + vp.proof = proof; + } - return { - '@context': context, - id, - type: types, - issuer, - previousCredentialId, - rootCredentialId: rootCredentialId || id, - credentialSubject: { - id: subjectId, - 'https://rdf.dock.io/alpha/2021#mayClaim': mayClaim, - ...additionalSubjectProperties, - }, - }; + return vp; } /** - * Creates an Ed25519 proof object for presentations - * @param params - Proof parameters - * @returns Proof object + * Re-export cedar for use in tests and external code */ -export function createEd25519Proof(params: { - verificationMethod: string; - challenge: string; - domain: string; - created?: string; - jws?: string; -}): any { - return { - type: 'Ed25519Signature2018', - created: params.created || new Date().toISOString(), - verificationMethod: params.verificationMethod, - proofPurpose: 'authentication', - challenge: params.challenge, - domain: params.domain, - jws: params.jws || 'placeholder..signature', - }; -} +export { cedar }; /** * Service class for delegatable credentials operations @@ -308,23 +405,79 @@ class DelegatableCredentialsService { name = 'delegatable-credentials'; rpcMethods = [ + DelegatableCredentialsService.prototype.issueDelegation, + DelegatableCredentialsService.prototype.issueDelegatedCredential, + DelegatableCredentialsService.prototype.createPresentation, DelegatableCredentialsService.prototype.verifyPresentation, DelegatableCredentialsService.prototype.createPolicy, - DelegatableCredentialsService.prototype.createPresentation, - DelegatableCredentialsService.prototype.createDelegation, ]; + /** + * Issues a delegation credential + */ + async issueDelegation(params: { + keyPair: KeyPair; + id: string; + issuerDid: string; + delegateDid: string; + mayClaim: string[]; + context?: any[]; + types?: string[]; + additionalSubjectProperties?: Record; + previousCredentialId?: string | null; + rootCredentialId?: string; + }): Promise { + return issueDelegationCredential(params.keyPair, params); + } + + /** + * Issues a credential as a delegate + */ + async issueDelegatedCredential(params: { + keyPair: KeyPair; + id: string; + issuerDid: string; + subjectDid: string; + claims: Record; + rootCredentialId: string; + previousCredentialId: string; + context?: any[]; + types?: string[]; + }): Promise { + return issueDelegatedCredential(params.keyPair, params); + } + + /** + * Creates and signs a verifiable presentation + */ + async createPresentation(params: { + keyPair: KeyPair; + credentials: any[]; + holderDid: string; + challenge: string; + domain: string; + context?: any[]; + }): Promise { + return createSignedPresentation(params.keyPair, params); + } + /** * Verifies a verifiable presentation with delegation chain */ async verifyPresentation(params: { presentation: VerifiablePresentation; + challenge?: string; + domain?: string; + unsignedPresentation?: boolean; + failOnUnauthorizedClaims?: boolean; policies?: CedarPolicies; - documentLoader?: (url: string) => Promise; }): Promise { return verifyDelegatablePresentation(params.presentation, { + challenge: params.challenge, + domain: params.domain, + unsignedPresentation: params.unsignedPresentation, + failOnUnauthorizedClaims: params.failOnUnauthorizedClaims, policies: params.policies, - documentLoader: params.documentLoader, }); } @@ -338,38 +491,6 @@ class DelegatableCredentialsService { }): CedarPolicies { return createCedarPolicy(params); } - - /** - * Creates a verifiable presentation for delegation - */ - createPresentation(params: { - credentials: any[]; - proof: any; - context?: any[]; - }): VerifiablePresentation { - return createDelegatablePresentation( - params.credentials, - params.proof, - params.context - ); - } - - /** - * Creates a delegation credential - */ - createDelegation(params: { - id: string; - context?: any[]; - types?: string[]; - issuer: string; - subjectId: string; - mayClaim: string[]; - additionalSubjectProperties?: Record; - previousCredentialId?: string | null; - rootCredentialId?: string; - }): DelegationCredential { - return createDelegationCredential(params); - } } export const delegatableCredentialsService = new DelegatableCredentialsService(); diff --git a/packages/wasm/src/services/credential/index.ts b/packages/wasm/src/services/credential/index.ts index dd377e5e..fb9bbf5a 100644 --- a/packages/wasm/src/services/credential/index.ts +++ b/packages/wasm/src/services/credential/index.ts @@ -3,3 +3,20 @@ import {credentialService} from './service'; // TODO: rename it to credentialService, will need to update dock-app export const credentialServiceRPC = credentialService; + +export { + delegatableCredentialsService, + verifyDelegatablePresentation, + issueDelegationCredential, + issueDelegatedCredential, + createSignedPresentation, + createUnsignedPresentation, + createCedarPolicy, + cedar, + MAY_CLAIM_IRI, + W3C_CREDENTIALS_V1, + DELEGATION_CONTEXT_URL, + DELEGATION_CREDENTIAL_CONTEXT, + CREDIT_SCORE_CONTEXT, + PRESENTATION_CONTEXT, +} from './delegatable-credentials'; From 29521632fe1a02a29e5432334530696908e26925 Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 23 Dec 2025 15:28:11 -0300 Subject: [PATCH 3/7] testing unauthorized scenario --- .../delegatable-credentials.test.ts | 93 +++++++++++++++++++ .../credential/delegatable-credentials.ts | 88 +++++++----------- .../wasm/src/services/credential/index.ts | 5 +- 3 files changed, 131 insertions(+), 55 deletions(-) diff --git a/integration-tests/delegatable-credentials.test.ts b/integration-tests/delegatable-credentials.test.ts index d81c5f91..1d6035ec 100644 --- a/integration-tests/delegatable-credentials.test.ts +++ b/integration-tests/delegatable-credentials.test.ts @@ -5,9 +5,43 @@ import { createSignedPresentation, createCedarPolicy, MAY_CLAIM_IRI, + W3C_CREDENTIALS_V1, + DELEGATION_CONTEXT_TERMS, } from '@docknetwork/wallet-sdk-wasm/src/services/credential/delegatable-credentials'; import { didService } from '@docknetwork/wallet-sdk-wasm/src/services/dids/service'; +// ============================================================================ +// TEST-SPECIFIC CONTEXTS (Credit Score Use Case) +// ============================================================================ + +/** + * Context for credit score delegation credentials + * Extends the base delegation terms with credit score specific vocabulary + */ +const CREDIT_SCORE_DELEGATION_CONTEXT = [ + W3C_CREDENTIALS_V1, + { + ...DELEGATION_CONTEXT_TERMS, + ex: 'https://example.org/credentials#', + CreditScoreDelegation: 'ex:CreditScoreDelegation', + body: 'ex:body', + }, +]; + +/** + * Context for credit score credentials issued by delegates + */ +const CREDIT_SCORE_CREDENTIAL_CONTEXT = [ + W3C_CREDENTIALS_V1, + { + ...DELEGATION_CONTEXT_TERMS, + ex: 'https://example.org/credentials#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + CreditScoreCredential: 'ex:CreditScoreCredential', + creditScore: { '@id': 'ex:creditScore', '@type': 'xsd:integer' }, + }, +]; + // ============================================================================ // TEST CONFIGURATION // ============================================================================ @@ -51,6 +85,8 @@ describe('Delegatable Credentials', () => { issuerDid: rootIssuerDid, delegateDid: delegateDid, mayClaim: ['creditScore'], + context: CREDIT_SCORE_DELEGATION_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], additionalSubjectProperties: { body: 'Issuer of Credit Scores', }, @@ -66,6 +102,8 @@ describe('Delegatable Credentials', () => { }, rootCredentialId: DELEGATION_ROOT_ID, previousCredentialId: DELEGATION_ROOT_ID, + context: CREDIT_SCORE_CREDENTIAL_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreCredential'], }); // Issue an unauthorized delegation (no creditScore in mayClaim) @@ -74,6 +112,8 @@ describe('Delegatable Credentials', () => { issuerDid: rootIssuerDid, delegateDid: delegateDid, mayClaim: ['someOtherClaim'], // Does NOT include creditScore + context: CREDIT_SCORE_DELEGATION_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], }); }); @@ -147,6 +187,59 @@ describe('Delegatable Credentials', () => { } }); + it('should deny unauthorized delegation (wrong mayClaim)', async () => { + // Create a credential that uses an unauthorized delegation + // The unauthorizedDelegationCredential only grants 'someOtherClaim', not 'creditScore' + const unauthorizedCreditScore = await issueDelegatedCredential(delegateKey, { + id: 'urn:cred:unauthorized-credit-score', + issuerDid: delegateDid, + subjectDid: SUBJECT_DID, + claims: { + creditScore: 500, + }, + rootCredentialId: 'urn:cred:unauthorized-delegation', + previousCredentialId: 'urn:cred:unauthorized-delegation', + context: CREDIT_SCORE_CREDENTIAL_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreCredential'], + }); + + // Create presentation with unauthorized delegation + const presentation = await createSignedPresentation(delegateKey, { + credentials: [unauthorizedDelegationCredential, unauthorizedCreditScore], + holderDid: delegateDid, + challenge: CHALLENGE, + domain: DOMAIN, + }); + + // Create Cedar policy that requires creditScore claim authorization + const policies = createCedarPolicy({ + maxDepth: 2, + rootIssuer: rootIssuerDid, + requiredClaims: { + creditScore: 0, + }, + }); + + // Verify the presentation - should fail because creditScore is not authorized + const result = await verifyDelegatablePresentation(presentation, { + challenge: CHALLENGE, + domain: DOMAIN, + policies, + }); + + console.log('Unauthorized delegation result:', { + verified: result.verified, + delegationDecision: result.delegationResult?.decision, + failures: result.delegationResult?.failures?.map(f => f.message), + }); + + // Delegation should be denied because the delegate doesn't have creditScore authority + expect(result.delegationResult?.decision).toBe('deny'); + expect(result.delegationResult?.failures).toBeDefined(); + expect(result.delegationResult?.failures?.length).toBeGreaterThan(0); + expect(result.delegationResult?.failures?.[0]?.code).toBe('UNAUTHORIZED_CLAIM'); + }); + it('should verify delegation without Cedar policies', async () => { // Create a signed presentation const presentation = await createSignedPresentation(delegateKey, { diff --git a/packages/wasm/src/services/credential/delegatable-credentials.ts b/packages/wasm/src/services/credential/delegatable-credentials.ts index 8d6e5cce..53063b22 100644 --- a/packages/wasm/src/services/credential/delegatable-credentials.ts +++ b/packages/wasm/src/services/credential/delegatable-credentials.ts @@ -91,59 +91,43 @@ export interface KeyPair { export const W3C_CREDENTIALS_V1 = 'https://www.w3.org/2018/credentials/v1'; /** - * Delegation context URL (for documentation purposes - we embed the context inline) + * Re-export MAY_CLAIM_IRI for use in credentials */ -export const DELEGATION_CONTEXT_URL = 'https://ld.truvera.io/credentials/delegation'; +export { MAY_CLAIM_IRI }; /** - * Re-export MAY_CLAIM_IRI for use in credentials + * Namespace used by the vc-delegation-engine for delegation properties */ -export { MAY_CLAIM_IRI }; +export const DELEGATION_ENGINE_NS = 'https://ld.truvera.io/credentials/delegation#'; /** - * Embedded delegation context terms - * This defines the JSON-LD terms needed for delegation credentials + * Base delegation context terms required for delegation credentials. + * These terms define the JSON-LD mappings needed for the vc-delegation-engine + * to properly process delegation chains. + * + * Use this as a base and extend with your own application-specific terms: + * @example + * const myContext = [ + * W3C_CREDENTIALS_V1, + * { + * ...DELEGATION_CONTEXT_TERMS, + * // Add your custom terms here + * MyCredentialType: 'https://example.org/MyCredentialType', + * myField: 'https://example.org/myField', + * }, + * ]; */ -const DELEGATION_CONTEXT_TERMS = { +export const DELEGATION_CONTEXT_TERMS = { '@version': 1.1, '@protected': true, - delegation: 'https://rdf.dock.io/credentials/delegation#', - DelegationCredential: 'delegation:DelegationCredential', + DelegationCredential: `${DELEGATION_ENGINE_NS}DelegationCredential`, mayClaim: { '@id': MAY_CLAIM_IRI, '@container': '@set' }, - rootCredentialId: { '@id': 'delegation:rootCredentialId', '@type': '@id' }, - previousCredentialId: { '@id': 'delegation:previousCredentialId', '@type': '@id' }, + rootCredentialId: { '@id': `${DELEGATION_ENGINE_NS}rootCredentialId`, '@type': '@id' }, + previousCredentialId: { '@id': `${DELEGATION_ENGINE_NS}previousCredentialId`, '@type': '@id' }, }; /** - * Pre-defined context for delegation credentials - */ -export const DELEGATION_CREDENTIAL_CONTEXT = [ - W3C_CREDENTIALS_V1, - { - ...DELEGATION_CONTEXT_TERMS, - dock: 'https://rdf.dock.io/alpha/2021#', - ex: 'https://example.org/credentials#', - CreditScoreDelegation: 'ex:CreditScoreDelegation', - body: 'ex:body', - }, -]; - -/** - * Pre-defined context for credit score credentials - */ -export const CREDIT_SCORE_CONTEXT = [ - W3C_CREDENTIALS_V1, - { - ...DELEGATION_CONTEXT_TERMS, - ex: 'https://example.org/credentials#', - xsd: 'http://www.w3.org/2001/XMLSchema#', - CreditScoreCredential: 'ex:CreditScoreCredential', - creditScore: { '@id': 'ex:creditScore', '@type': 'xsd:integer' }, - }, -]; - -/** - * Pre-defined context for verifiable presentations + * Default context for verifiable presentations */ export const PRESENTATION_CONTEXT = [W3C_CREDENTIALS_V1]; @@ -160,8 +144,8 @@ export async function issueDelegationCredential( issuerDid: string; delegateDid: string; mayClaim: string[]; - context?: any[]; - types?: string[]; + context: any[]; + types: string[]; additionalSubjectProperties?: Record; previousCredentialId?: string | null; rootCredentialId?: string; @@ -172,8 +156,8 @@ export async function issueDelegationCredential( issuerDid, delegateDid, mayClaim, - context = DELEGATION_CREDENTIAL_CONTEXT, - types = ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], + context, + types, additionalSubjectProperties = {}, previousCredentialId = null, rootCredentialId, @@ -213,8 +197,8 @@ export async function issueDelegatedCredential( claims: Record; rootCredentialId: string; previousCredentialId: string; - context?: any[]; - types?: string[]; + context: any[]; + types: string[]; } ): Promise { const { @@ -224,8 +208,8 @@ export async function issueDelegatedCredential( claims, rootCredentialId, previousCredentialId, - context = CREDIT_SCORE_CONTEXT, - types = ['VerifiableCredential', 'CreditScoreCredential'], + context, + types, } = params; const credential = { @@ -421,8 +405,8 @@ class DelegatableCredentialsService { issuerDid: string; delegateDid: string; mayClaim: string[]; - context?: any[]; - types?: string[]; + context: any[]; + types: string[]; additionalSubjectProperties?: Record; previousCredentialId?: string | null; rootCredentialId?: string; @@ -441,8 +425,8 @@ class DelegatableCredentialsService { claims: Record; rootCredentialId: string; previousCredentialId: string; - context?: any[]; - types?: string[]; + context: any[]; + types: string[]; }): Promise { return issueDelegatedCredential(params.keyPair, params); } diff --git a/packages/wasm/src/services/credential/index.ts b/packages/wasm/src/services/credential/index.ts index fb9bbf5a..d768a444 100644 --- a/packages/wasm/src/services/credential/index.ts +++ b/packages/wasm/src/services/credential/index.ts @@ -15,8 +15,7 @@ export { cedar, MAY_CLAIM_IRI, W3C_CREDENTIALS_V1, - DELEGATION_CONTEXT_URL, - DELEGATION_CREDENTIAL_CONTEXT, - CREDIT_SCORE_CONTEXT, + DELEGATION_ENGINE_NS, + DELEGATION_CONTEXT_TERMS, PRESENTATION_CONTEXT, } from './delegatable-credentials'; From 9f345a95829a13091fc96e119a83a5a21e1351fb Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 23 Dec 2025 15:35:34 -0300 Subject: [PATCH 4/7] fixing module resolution issue --- jest.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jest.config.js b/jest.config.js index f1b7ea3c..3c6d652e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -32,6 +32,8 @@ module.exports = { moduleNameMapper: { '@digitalbazaar/minimal-cipher': '@digitalbazaar/minimal-cipher/Cipher', '@digitalbazaar/did-method-key': '@digitalbazaar/did-method-key/lib/main', + '@digitalbazaar/http-client': + '/node_modules/@digitalbazaar/http-client/dist/cjs/index.cjs', '@docknetwork/wallet-sdk-wasm/lib/(.*)': '@docknetwork/wallet-sdk-wasm/src/$1', '@docknetwork/wallet-sdk-data-store/lib/(.*)': From 20ae2f12c7b2594a270422c8eca21088536d8c36 Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 23 Dec 2025 16:00:31 -0300 Subject: [PATCH 5/7] delegation credential should have rootId as undefined --- integration-tests/delegatable-credentials.test.ts | 2 +- .../wasm/src/services/credential/delegatable-credentials.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/delegatable-credentials.test.ts b/integration-tests/delegatable-credentials.test.ts index 1d6035ec..cd2f71f3 100644 --- a/integration-tests/delegatable-credentials.test.ts +++ b/integration-tests/delegatable-credentials.test.ts @@ -124,7 +124,7 @@ describe('Delegatable Credentials', () => { expect(delegationCredential.credentialSubject.id).toBe(delegateDid); expect(delegationCredential.credentialSubject[MAY_CLAIM_IRI]).toContain('creditScore'); expect(delegationCredential.proof).toBeDefined(); - expect(delegationCredential.rootCredentialId).toBe(DELEGATION_ROOT_ID); + expect(delegationCredential.rootCredentialId).toBeUndefined(); expect(delegationCredential.previousCredentialId).toBeNull(); }); diff --git a/packages/wasm/src/services/credential/delegatable-credentials.ts b/packages/wasm/src/services/credential/delegatable-credentials.ts index 53063b22..113b56bc 100644 --- a/packages/wasm/src/services/credential/delegatable-credentials.ts +++ b/packages/wasm/src/services/credential/delegatable-credentials.ts @@ -174,7 +174,7 @@ export async function issueDelegationCredential( [MAY_CLAIM_IRI]: mayClaim, ...additionalSubjectProperties, }, - rootCredentialId: rootCredentialId || id, + rootCredentialId: undefined, previousCredentialId, }; From c954014ca9988a9978de5d173a7ddcacf1ea0f3a Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 23 Dec 2025 16:20:19 -0300 Subject: [PATCH 6/7] fixing module resolution issue --- jest.config.e2e.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jest.config.e2e.js b/jest.config.e2e.js index 624a4833..5c687521 100644 --- a/jest.config.e2e.js +++ b/jest.config.e2e.js @@ -26,6 +26,7 @@ module.exports = { globalTeardown: './scripts/integration-test-teardown.js', setupFiles: ['jest-localstorage-mock'], moduleNameMapper: { + 'ky-universal': 'ky', '@digitalbazaar/edv-client': require.resolve( '@digitalbazaar/edv-client/main.js', ), @@ -50,6 +51,6 @@ module.exports = { '@docknetwork/wallet-sdk-data-store/src', }, transformIgnorePatterns: [ - '/node_modules/(?!@babel|@docknetwork|@digitalbazaar|base58-universal|multiformats|p-limit|yocto-queue|@cheqd/ts-proto)', + '/node_modules/(?!@babel|@docknetwork|@digitalbazaar|base58-universal|multiformats|p-limit|yocto-queue|@cheqd/ts-proto|ky)', ], }; From 5b8d2502fc4e604d47b42a95d6c76648614fd40b Mon Sep 17 00:00:00 2001 From: Maycon Date: Tue, 23 Dec 2025 17:07:58 -0300 Subject: [PATCH 7/7] should create a valid presentation with a delegated credential --- .../delegatable-credentials.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/integration-tests/delegatable-credentials.test.ts b/integration-tests/delegatable-credentials.test.ts index cd2f71f3..7d3e0408 100644 --- a/integration-tests/delegatable-credentials.test.ts +++ b/integration-tests/delegatable-credentials.test.ts @@ -139,6 +139,22 @@ describe('Delegatable Credentials', () => { expect(creditScoreCredential.previousCredentialId).toBe(DELEGATION_ROOT_ID); }); + it('should create a valid presentation with a delegated credential', async () => { + const presentation = await createSignedPresentation(delegateKey, { + credentials: [delegationCredential], + holderDid: delegateDid, + challenge: CHALLENGE, + domain: DOMAIN, + }); + + const result = await verifyDelegatablePresentation(presentation, { + challenge: CHALLENGE, + domain: DOMAIN, + }); + + expect(result.verified).toBe(true); + }); + it('should verify authorized delegation with Cedar policies', async () => { // Create a signed presentation with both credentials const presentation = await createSignedPresentation(delegateKey, {