diff --git a/integration-tests/delegatable-credentials.test.ts b/integration-tests/delegatable-credentials.test.ts new file mode 100644 index 00000000..7d3e0408 --- /dev/null +++ b/integration-tests/delegatable-credentials.test.ts @@ -0,0 +1,298 @@ +import { + verifyDelegatablePresentation, + issueDelegationCredential, + issueDelegatedCredential, + 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 +// ============================================================================ + +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', () => { + 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', + }); + + // Extract DIDs from the key documents + rootIssuerDid = rootIssuerKey.controller; + delegateDid = delegateKey.controller; + + console.log('Root Issuer DID:', rootIssuerDid); + console.log('Delegate DID:', delegateDid); + + // 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'], + context: CREDIT_SCORE_DELEGATION_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], + 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, + context: CREDIT_SCORE_CREDENTIAL_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreCredential'], + }); + + // 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 + context: CREDIT_SCORE_DELEGATION_CONTEXT, + types: ['VerifiableCredential', 'CreditScoreDelegation', 'DelegationCredential'], + }); + }); + + 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).toBeUndefined(); + expect(delegationCredential.previousCredentialId).toBeNull(); + }); + + 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 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, { + credentials: [delegationCredential, creditScoreCredential], + holderDid: delegateDid, + challenge: CHALLENGE, + domain: DOMAIN, + }); + + // Create Cedar policy that allows this delegation + const policies = createCedarPolicy({ + maxDepth: 2, + rootIssuer: rootIssuerDid, + requiredClaims: { + creditScore: 0, + body: 'Issuer of Credit Scores', + }, + }); + + // 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), + }); + + // 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 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, { + credentials: [delegationCredential, creditScoreCredential], + holderDid: delegateDid, + challenge: CHALLENGE, + domain: DOMAIN, + }); + + // Verify without Cedar policies - just validates delegation chain + const result = await verifyDelegatablePresentation(presentation, { + challenge: CHALLENGE, + domain: DOMAIN, + }); + + console.log('Verification without policies:', { + verified: result.verified, + delegationDecision: result.delegationResult?.decision, + }); + + 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:example:root', + requiredClaims: { + level: 5, + role: 'admin', + }, + }); + + expect(policy.staticPolicies).toContain('context.tailDepth <= 3'); + 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/jest.config.e2e.js b/jest.config.e2e.js index 1e16d8df..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', ), @@ -40,9 +41,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/(.*)': @@ -51,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)', ], }; 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/(.*)': 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/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..113b56bc --- /dev/null +++ b/packages/wasm/src/services/credential/delegatable-credentials.ts @@ -0,0 +1,480 @@ +// @ts-nocheck +import * as cedar from '@cedar-policy/cedar-wasm/nodejs'; +import { + 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 { + verified: boolean; + credentialResults?: any[]; + delegationResult?: { + decision: string; + summaries?: any[]; + authorizations?: any[]; + failures?: any[]; + }; + error?: 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; + 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; +} + +/** + * W3C Credentials V1 context URL + */ +export const W3C_CREDENTIALS_V1 = 'https://www.w3.org/2018/credentials/v1'; + +/** + * Re-export MAY_CLAIM_IRI for use in credentials + */ +export { MAY_CLAIM_IRI }; + +/** + * Namespace used by the vc-delegation-engine for delegation properties + */ +export const DELEGATION_ENGINE_NS = 'https://ld.truvera.io/credentials/delegation#'; + +/** + * 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', + * }, + * ]; + */ +export const DELEGATION_CONTEXT_TERMS = { + '@version': 1.1, + '@protected': true, + DelegationCredential: `${DELEGATION_ENGINE_NS}DelegationCredential`, + mayClaim: { '@id': MAY_CLAIM_IRI, '@container': '@set' }, + rootCredentialId: { '@id': `${DELEGATION_ENGINE_NS}rootCredentialId`, '@type': '@id' }, + previousCredentialId: { '@id': `${DELEGATION_ENGINE_NS}previousCredentialId`, '@type': '@id' }, +}; + +/** + * Default 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, + types, + additionalSubjectProperties = {}, + previousCredentialId = null, + rootCredentialId, + } = params; + + const credential = { + '@context': context, + id, + type: types, + issuer: issuerDid, + issuanceDate: new Date().toISOString(), + credentialSubject: { + id: delegateDid, + [MAY_CLAIM_IRI]: mayClaim, + ...additionalSubjectProperties, + }, + rootCredentialId: undefined, + previousCredentialId, + }; + + const preparedKey = prepareKeyForSigning(keyPair); + return issueCredential(preparedKey, credential); +} + +/** + * 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 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, + types, + } = params; + + const credential = { + '@context': context, + id, + type: types, + issuer: issuerDid, + issuanceDate: new Date().toISOString(), + credentialSubject: { + id: subjectDid, + ...claims, + }, + rootCredentialId, + previousCredentialId, + }; + + const preparedKey = prepareKeyForSigning(keyPair); + return issueCredential(preparedKey, credential); +} + +/** + * 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); +} + +/** + * 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, + }; + + // Add Cedar authorization if policies are provided + if (policies) { + verifyOptions.cedarAuth = { + policies, + cedar, + }; + } + + return verifyPresentation(vp, verifyOptions); +} + +/** + * 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 an unsigned verifiable presentation (for testing) + * @param credentials - Array of credentials to include + * @param proof - Optional proof object + * @param context - Optional context + * @returns Verifiable presentation object + */ +export function createUnsignedPresentation( + credentials: any[], + proof?: any, + context: any[] = PRESENTATION_CONTEXT +): VerifiablePresentation { + const vp: VerifiablePresentation = { + '@context': context, + type: ['VerifiablePresentation'], + verifiableCredential: credentials, + }; + + if (proof) { + vp.proof = proof; + } + + return vp; +} + +/** + * Re-export cedar for use in tests and external code + */ +export { cedar }; + +/** + * Service class for delegatable credentials operations + */ +class DelegatableCredentialsService { + name = 'delegatable-credentials'; + + rpcMethods = [ + DelegatableCredentialsService.prototype.issueDelegation, + DelegatableCredentialsService.prototype.issueDelegatedCredential, + DelegatableCredentialsService.prototype.createPresentation, + DelegatableCredentialsService.prototype.verifyPresentation, + DelegatableCredentialsService.prototype.createPolicy, + ]; + + /** + * 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; + }): Promise { + return verifyDelegatablePresentation(params.presentation, { + challenge: params.challenge, + domain: params.domain, + unsignedPresentation: params.unsignedPresentation, + failOnUnauthorizedClaims: params.failOnUnauthorizedClaims, + policies: params.policies, + }); + } + + /** + * Creates a Cedar policy for delegation verification + */ + createPolicy(params: { + maxDepth?: number; + rootIssuer: string; + requiredClaims?: Record; + }): CedarPolicies { + return createCedarPolicy(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..d768a444 100644 --- a/packages/wasm/src/services/credential/index.ts +++ b/packages/wasm/src/services/credential/index.ts @@ -3,3 +3,19 @@ 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_ENGINE_NS, + DELEGATION_CONTEXT_TERMS, + PRESENTATION_CONTEXT, +} from './delegatable-credentials';