From 7ba97d013904777ceaa8ee57ea25d731905bf638 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 19 May 2024 18:37:46 +0800 Subject: [PATCH 1/5] feat: verify status list and fixes Signed-off-by: Timo Glastra --- packages/core/package.json | 10 +- .../src/modules/sd-jwt-vc/SdJwtVcService.ts | 202 ++++++++--- .../__tests__/SdJwtVcService.test.ts | 325 ++++++++++++++++-- .../sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts | 42 ++- packages/core/src/utils/fetch.ts | 28 ++ yarn.lock | 69 +++- 6 files changed, 574 insertions(+), 102 deletions(-) create mode 100644 packages/core/src/utils/fetch.ts diff --git a/packages/core/package.json b/packages/core/package.json index b496cba0cf..cd2662d8a5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,10 +27,12 @@ "@digitalcredentials/jsonld-signatures": "^9.4.0", "@digitalcredentials/vc": "^6.0.1", "@multiformats/base-x": "^4.0.1", - "@sd-jwt/core": "^0.6.1", - "@sd-jwt/decode": "^0.6.1", - "@sd-jwt/types": "^0.6.1", - "@sd-jwt/utils": "^0.6.1", + "@sd-jwt/core": "^0.7.0", + "@sd-jwt/decode": "^0.7.0", + "@sd-jwt/jwt-status-list": "^0.7.0", + "@sd-jwt/sd-jwt-vc": "^0.7.0", + "@sd-jwt/types": "^0.7.0", + "@sd-jwt/utils": "^0.7.0", "@sphereon/pex": "^3.3.2", "@sphereon/pex-models": "^2.2.4", "@sphereon/ssi-types": "^0.23.0", diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts index 833bff9f31..23c29a7c16 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -13,13 +13,14 @@ import type { Query } from '../../storage/StorageService' import type { SDJwt } from '@sd-jwt/core' import type { Signer, Verifier, HasherSync, PresentationFrame, DisclosureFrame } from '@sd-jwt/types' -import { SDJwtInstance } from '@sd-jwt/core' import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode' +import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc' import { uint8ArrayToBase64Url } from '@sd-jwt/utils' import { injectable } from 'tsyringe' -import { Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto' +import { JwtPayload, Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto' import { TypedArrayEncoder, Hasher } from '../../utils' +import { fetchWithTimeout } from '../../utils/fetch' import { DidResolverService, parseDid, getKeyFromVerificationMethod } from '../dids' import { SdJwtVcError } from './SdJwtVcError' @@ -44,7 +45,9 @@ export interface CnfPayload { export interface VerificationResult { isValid: boolean - isSignatureValid: boolean + isValidJwtPayload?: boolean + isSignatureValid?: boolean + isStatusValid?: boolean isNotBeforeValid?: boolean isExpiryTimeValid?: boolean areRequiredClaimsIncluded?: boolean @@ -85,16 +88,27 @@ export class SdJwtVcService { kid: issuer.kid, } as const - const sdjwt = new SDJwtInstance({ - hasher: this.hasher, + const sdjwt = new SDJwtVcInstance({ + ...this.getBaseSdJwtConfig(agentContext), signer: this.signer(agentContext, issuer.key), hashAlg: 'sha-256', signAlg: issuer.alg, saltGenerator: agentContext.wallet.generateNonce, }) + if (!payload.vct || typeof payload.vct !== 'string') { + throw new SdJwtVcError("Missing required parameter 'vct'") + } + console.log(payload) + const compact = await sdjwt.issue( - { ...payload, cnf: holderBinding?.cnf, iss: issuer.iss, iat: Math.floor(new Date().getTime() / 1000) }, + { + ...payload, + cnf: holderBinding?.cnf, + iss: issuer.iss, + iat: Math.floor(new Date().getTime() / 1000), + vct: payload.vct, + }, disclosureFrame as DisclosureFrame, { header } ) @@ -133,9 +147,8 @@ export class SdJwtVcService { agentContext: AgentContext, { compactSdJwtVc, presentationFrame, verifierMetadata }: SdJwtVcPresentOptions ): Promise { - const sdjwt = new SDJwtInstance({ - hasher: this.hasher, - }) + const sdjwt = new SDJwtVcInstance(this.getBaseSdJwtConfig(agentContext)) + const sdJwtVc = await sdjwt.decode(compactSdJwtVc) const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc) @@ -167,69 +180,124 @@ export class SdJwtVcService { public async verify
( agentContext: AgentContext, { compactSdJwtVc, keyBinding, requiredClaimKeys }: SdJwtVcVerifyOptions - ) { - const sdjwt = new SDJwtInstance({ - hasher: this.hasher, - }) - const sdJwtVc = await sdjwt.decode(compactSdJwtVc) - if (!sdJwtVc.jwt) { - throw new SdJwtVcError('Invalid sd-jwt-vc state.') - } - - const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc)) - const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc) - const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined - - sdjwt.config({ - verifier: this.verifier(agentContext, issuer.key), - kbVerifier: holder ? this.verifier(agentContext, holder.key) : undefined, - }) + ): Promise< + | { isValid: true; verification: VerificationResult; sdJwtVc: SdJwtVc } + | { isValid: false; verification: VerificationResult; sdJwtVc?: SdJwtVc; error: Error } + > { + const sdjwt = new SDJwtVcInstance(this.getBaseSdJwtConfig(agentContext)) const verificationResult: VerificationResult = { isValid: false, - isSignatureValid: false, } - await sdjwt.verify(compactSdJwtVc, requiredClaimKeys, keyBinding !== undefined) + let sdJwtVc: SDJwt - verificationResult.isValid = true - verificationResult.isSignatureValid = true - verificationResult.areRequiredClaimsIncluded = true + try { + sdJwtVc = await sdjwt.decode(compactSdJwtVc) + if (!sdJwtVc.jwt) throw new Error('Invalid sd-jwt-vc') + } catch (error) { + return { + isValid: false, + verification: verificationResult, + error, + } + } + + const returnSdJwtVc: SdJwtVc = { + payload: sdJwtVc.jwt.payload as Payload, + header: sdJwtVc.jwt.header as Header, + compact: compactSdJwtVc, + prettyClaims: await sdJwtVc.getClaims(this.hasher), + } satisfies SdJwtVc - // If keyBinding is present, verify the key binding try { - if (keyBinding) { - if (!sdJwtVc.kbJwt || !sdJwtVc.kbJwt.payload) { - throw new SdJwtVcError('Keybinding is required for verification of the sd-jwt-vc') - } + const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc)) + const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc) + const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined + + sdjwt.config({ + verifier: this.verifier(agentContext, issuer.key), + kbVerifier: holder ? this.verifier(agentContext, holder.key) : undefined, + }) - // Assert `aud` and `nonce` claims - if (sdJwtVc.kbJwt.payload.aud !== keyBinding.audience) { - throw new SdJwtVcError('The key binding JWT does not contain the expected audience') + const requiredKeys = requiredClaimKeys ? [...requiredClaimKeys, 'vct'] : ['vct'] + + try { + await sdjwt.verify(compactSdJwtVc, requiredKeys, keyBinding !== undefined) + + verificationResult.isSignatureValid = true + verificationResult.areRequiredClaimsIncluded = true + verificationResult.isStatusValid = true + } catch (error) { + return { + verification: verificationResult, + error, + isValid: false, + sdJwtVc: returnSdJwtVc, } + } - if (sdJwtVc.kbJwt.payload.nonce !== keyBinding.nonce) { - throw new SdJwtVcError('The key binding JWT does not contain the expected nonce') + try { + JwtPayload.fromJson(returnSdJwtVc.payload).validate() + verificationResult.isValidJwtPayload = true + } catch (error) { + verificationResult.isValidJwtPayload = false + + return { + isValid: false, + error, + verification: verificationResult, + sdJwtVc: returnSdJwtVc, } + } + + // If keyBinding is present, verify the key binding + try { + if (keyBinding) { + if (!sdJwtVc.kbJwt || !sdJwtVc.kbJwt.payload) { + throw new SdJwtVcError('Keybinding is required for verification of the sd-jwt-vc') + } + + // Assert `aud` and `nonce` claims + if (sdJwtVc.kbJwt.payload.aud !== keyBinding.audience) { + throw new SdJwtVcError('The key binding JWT does not contain the expected audience') + } + + if (sdJwtVc.kbJwt.payload.nonce !== keyBinding.nonce) { + throw new SdJwtVcError('The key binding JWT does not contain the expected nonce') + } - verificationResult.isKeyBindingValid = true - verificationResult.containsExpectedKeyBinding = true - verificationResult.containsRequiredVcProperties = true + verificationResult.isKeyBindingValid = true + verificationResult.containsExpectedKeyBinding = true + verificationResult.containsRequiredVcProperties = true + } + } catch (error) { + verificationResult.isKeyBindingValid = false + verificationResult.containsExpectedKeyBinding = false + verificationResult.isValid = false + + return { + isValid: false, + error, + verification: verificationResult, + sdJwtVc: returnSdJwtVc, + } } } catch (error) { - verificationResult.isKeyBindingValid = false - verificationResult.containsExpectedKeyBinding = false verificationResult.isValid = false + return { + isValid: false, + error, + verification: verificationResult, + sdJwtVc: returnSdJwtVc, + } } + verificationResult.isValid = true return { + isValid: true, verification: verificationResult, - sdJwtVc: { - payload: sdJwtVc.jwt.payload as Payload, - header: sdJwtVc.jwt.header as Header, - compact: compactSdJwtVc, - prettyClaims: await sdJwtVc.getClaims(this.hasher), - } satisfies SdJwtVc, + sdJwtVc: returnSdJwtVc, } } @@ -272,10 +340,6 @@ export class SdJwtVcService { } } - private get hasher(): HasherSync { - return Hasher.hash - } - /** * @todo validate the JWT header (alg) */ @@ -448,4 +512,30 @@ export class SdJwtVcService { throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.") } + + private getBaseSdJwtConfig(agentContext: AgentContext) { + return { + hasher: this.hasher, + statusListFetcher: this.getStatusListFetcher(agentContext), + } + } + + private get hasher(): HasherSync { + return Hasher.hash + } + + private getStatusListFetcher(agentContext: AgentContext) { + return async (uri: string) => { + const response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, uri) + if (!response.ok) { + throw new Error( + `Received invalid response with status ${ + response.status + } when fetching status list from ${uri}. ${await response.text()}` + ) + } + + return await response.text() + } + } } diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts index c49f48e772..7751e1cac2 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts @@ -1,31 +1,42 @@ import type { SdJwtVcHeader } from '../SdJwtVcOptions' -import type { Jwk, Key } from '@credo-ts/core' +import type { AgentContext, Jwk, Key } from '@credo-ts/core' +import { createHeaderAndPayload, StatusList } from '@sd-jwt/jwt-status-list' +import { SDJWTException } from '@sd-jwt/utils' import { randomUUID } from 'crypto' -import { getInMemoryAgentOptions } from '../../../../tests' +import { agentDependencies, getInMemoryAgentOptions } from '../../../../tests' +import * as fetchUtils from '../../../utils/fetch' import { SdJwtVcService } from '../SdJwtVcService' import { SdJwtVcRepository } from '../repository' import { complexSdJwtVc, complexSdJwtVcPresentation, + contentChangedSdJwtVc, + expiredSdJwtVc, + notBeforeInFutureSdJwtVc, sdJwtVcWithSingleDisclosure, sdJwtVcWithSingleDisclosurePresentation, + signatureInvalidSdJwtVc, simpleJwtVc, simpleJwtVcPresentation, simpleJwtVcWithoutHolderBinding, + simpleSdJwtVcWithStatus, } from './sdjwtvc.fixtures' import { - parseDid, - getJwkFromKey, + CredoError, + Agent, DidKey, DidsModule, + getJwkFromKey, + JwsService, + JwtPayload, KeyDidRegistrar, KeyDidResolver, KeyType, - Agent, + parseDid, TypedArrayEncoder, } from '@credo-ts/core' @@ -54,6 +65,41 @@ Date.prototype.getTime = jest.fn(() => 1698151532000) jest.mock('../repository/SdJwtVcRepository') const SdJwtVcRepositoryMock = SdJwtVcRepository as jest.Mock +const generateStatusList = async ( + agentContext: AgentContext, + key: Key, + issuerDidUrl: string, + length: number, + revokedIndexes: number[] +): Promise => { + const statusList = new StatusList( + Array.from({ length }, (_, i) => (revokedIndexes.includes(i) ? 1 : 0)), + 1 + ) + + const [did, keyId] = issuerDidUrl.split('#') + const { header, payload } = createHeaderAndPayload( + statusList, + { + iss: did, + sub: 'https://example.com/status/1', + iat: new Date().getTime() / 1000, + }, + { + alg: 'EdDSA', + typ: 'statuslist+jwt', + kid: `#${keyId}`, + } + ) + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + return jwsService.createJwsCompact(agentContext, { + key, + payload: JwtPayload.fromJson(payload), + protectedHeaderOptions: header, + }) +} + describe('SdJwtVcService', () => { const verifierDid = 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y' let issuerDidUrl: string @@ -658,19 +704,25 @@ describe('SdJwtVcService', () => { }, }) - const { verification } = await sdJwtVcService.verify(agent.context, { + const verificationResult = await sdJwtVcService.verify(agent.context, { compactSdJwtVc: presentation, keyBinding: { audience: verifierDid, nonce }, requiredClaimKeys: ['claim'], }) - expect(verification).toEqual({ - isSignatureValid: true, - containsRequiredVcProperties: true, - containsExpectedKeyBinding: true, - areRequiredClaimsIncluded: true, + expect(verificationResult).toEqual({ isValid: true, - isKeyBindingValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + containsRequiredVcProperties: true, + containsExpectedKeyBinding: true, + areRequiredClaimsIncluded: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + isKeyBindingValid: true, + }, }) }) @@ -681,15 +733,148 @@ describe('SdJwtVcService', () => { presentationFrame: {}, }) - const { verification } = await sdJwtVcService.verify(agent.context, { + const verificationResult = await sdJwtVcService.verify(agent.context, { compactSdJwtVc: presentation, requiredClaimKeys: ['claim'], }) - expect(verification).toEqual({ - isSignatureValid: true, - areRequiredClaimsIncluded: true, + expect(verificationResult).toEqual({ + isValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + areRequiredClaimsIncluded: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + }, + }) + }) + + test('Verify sd-jwt-vc with status where credential is not revoked', async () => { + const sdJwtVcService = agent.dependencyManager.resolve(SdJwtVcService) + // const { compact } = await sdJwtVcService.sign(agent.context, { + // payload: { + // claim: 'some-claim', + // vct: 'IdentityCredential', + // status: { + // status_list: { + // idx: 12, + // uri: 'https://example.com/status-list', + // }, + // }, + // }, + // issuer: { + // method: 'did', + // didUrl: issuerDidUrl, + // }, + // }) + + // Mock call to status list + const fetchSpy = jest.spyOn(fetchUtils, 'fetchWithTimeout') + + // First time not revoked + fetchSpy.mockResolvedValueOnce({ + ok: true, + status: 200, + text: () => generateStatusList(agent.context, issuerKey, issuerDidUrl, 24, []), + } satisfies Partial as Response) + + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleSdJwtVcWithStatus, + presentationFrame: {}, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + }) + expect(fetchUtils.fetchWithTimeout).toHaveBeenCalledWith( + agentDependencies.fetch, + 'https://example.com/status-list' + ) + + expect(verificationResult).toEqual({ isValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + areRequiredClaimsIncluded: true, + }, + }) + }) + + test('Verify sd-jwt-vc with status where credential is revoked and fails', async () => { + const sdJwtVcService = agent.dependencyManager.resolve(SdJwtVcService) + + // Mock call to status list + const fetchSpy = jest.spyOn(fetchUtils, 'fetchWithTimeout') + + // First time not revoked + fetchSpy.mockResolvedValueOnce({ + ok: true, + status: 200, + text: () => generateStatusList(agent.context, issuerKey, issuerDidUrl, 24, [12]), + } satisfies Partial as Response) + + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleSdJwtVcWithStatus, + presentationFrame: {}, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + }) + expect(fetchUtils.fetchWithTimeout).toHaveBeenCalledWith( + agentDependencies.fetch, + 'https://example.com/status-list' + ) + + expect(verificationResult).toEqual({ + isValid: false, + sdJwtVc: expect.any(Object), + verification: { + isValid: false, + }, + error: new SDJWTException('Status is not valid'), + }) + }) + + test('Verify sd-jwt-vc with status where status list is not valid and fails', async () => { + const sdJwtVcService = agent.dependencyManager.resolve(SdJwtVcService) + + // Mock call to status list + const fetchSpy = jest.spyOn(fetchUtils, 'fetchWithTimeout') + + // First time not revoked + fetchSpy.mockResolvedValueOnce({ + ok: true, + status: 200, + text: () => generateStatusList(agent.context, issuerKey, issuerDidUrl, 8, []), + } satisfies Partial as Response) + + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleSdJwtVcWithStatus, + presentationFrame: {}, + }) + + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + }) + expect(fetchUtils.fetchWithTimeout).toHaveBeenCalledWith( + agentDependencies.fetch, + 'https://example.com/status-list' + ) + + expect(verificationResult).toEqual({ + isValid: false, + sdJwtVc: expect.any(Object), + verification: { + isValid: false, + }, + error: new Error('Index out of bounds'), }) }) @@ -706,19 +891,25 @@ describe('SdJwtVcService', () => { presentationFrame: { claim: true }, }) - const { verification } = await sdJwtVcService.verify(agent.context, { + const verificationResult = await sdJwtVcService.verify(agent.context, { compactSdJwtVc: presentation, keyBinding: { audience: verifierDid, nonce }, requiredClaimKeys: ['vct', 'cnf', 'claim', 'iat'], }) - expect(verification).toEqual({ - isSignatureValid: true, - containsRequiredVcProperties: true, - areRequiredClaimsIncluded: true, + expect(verificationResult).toEqual({ isValid: true, - isKeyBindingValid: true, - containsExpectedKeyBinding: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + containsRequiredVcProperties: true, + areRequiredClaimsIncluded: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + isKeyBindingValid: true, + containsExpectedKeyBinding: true, + }, }) }) @@ -749,7 +940,7 @@ describe('SdJwtVcService', () => { }, }) - const { verification } = await sdJwtVcService.verify(agent.context, { + const verificationResult = await sdJwtVcService.verify(agent.context, { compactSdJwtVc: presentation, keyBinding: { audience: verifierDid, nonce }, // FIXME: this should be a requiredFrame to be consistent with the other methods @@ -772,13 +963,19 @@ describe('SdJwtVcService', () => { ], }) - expect(verification).toEqual({ - isSignatureValid: true, - areRequiredClaimsIncluded: true, - containsExpectedKeyBinding: true, - containsRequiredVcProperties: true, + expect(verificationResult).toEqual({ isValid: true, - isKeyBindingValid: true, + sdJwtVc: expect.any(Object), + verification: { + isSignatureValid: true, + areRequiredClaimsIncluded: true, + containsExpectedKeyBinding: true, + containsRequiredVcProperties: true, + isValid: true, + isValidJwtPayload: true, + isStatusValid: true, + isKeyBindingValid: true, + }, }) }) @@ -797,5 +994,73 @@ describe('SdJwtVcService', () => { expect(verificationResult.verification.isValid).toBe(true) }) + + test('verify expired sd-jwt-vc and fails', async () => { + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: expiredSdJwtVc, + }) + + expect(verificationResult).toEqual({ + isValid: false, + verification: { + areRequiredClaimsIncluded: true, + isSignatureValid: true, + isStatusValid: true, + isValid: false, + isValidJwtPayload: false, + }, + error: new CredoError('JWT expired at 1716111919'), + sdJwtVc: expect.any(Object), + }) + }) + + test('verify sd-jwt-vc with nbf in future and fails', async () => { + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: notBeforeInFutureSdJwtVc, + }) + + expect(verificationResult).toEqual({ + isValid: false, + verification: { + areRequiredClaimsIncluded: true, + isSignatureValid: true, + isStatusValid: true, + isValid: false, + isValidJwtPayload: false, + }, + error: new CredoError('JWT not valid before 4078944000'), + sdJwtVc: expect.any(Object), + }) + }) + + test('verify sd-jwt-vc with content changed and fails', async () => { + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: contentChangedSdJwtVc, + }) + + expect(verificationResult).toEqual({ + isValid: false, + verification: { + isValid: false, + }, + error: new SDJWTException('Verify Error: Invalid JWT Signature'), + sdJwtVc: expect.any(Object), + }) + }) + + test('verify sd-jwt-vc with invalid signature and fails', async () => { + const verificationResult = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: signatureInvalidSdJwtVc, + }) + + expect(verificationResult).toEqual({ + isValid: false, + verification: { + isValid: false, + }, + error: new SDJWTException('Verify Error: Invalid JWT Signature'), + sdJwtVc: expect.any(Object), + }) + }) }) }) diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts index 12bb9247b0..56cf7e1902 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts @@ -27,6 +27,46 @@ export const simpleJwtVc = 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg~' +export const expiredSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJleHAiOjE3MTYxMTE5MTksImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyfQ.hOQ-CnT-iaL2_Dlui0NgVhBk2Lej4_AqDrEK-7bQNT2b6mJkaikvUXdNtg-z7GnCUNrjq35vm5ProqiyYQz_AA~' + +export const notBeforeInFutureSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJuYmYiOjQwNzg5NDQwMDAsImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyfQ.u0GPVCt7gPTrvT3sAwXxwkKW_Zy6YRRTaVRkrcSWt9VPonxQHUua2ggOERAu5cgtLeSdXzyqvS8nE9xFJg7xCw~' + +export const contentChangedSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwyIiwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.TsFJUFKwdw5kVL4eY5vHOPGHqXBCFJ-n9c9KwPHkXAVfZ1TZkGA8m0_sNuTDy5n_pCutS6uzKJDAM0dfeGPyDg~' + +export const signatureInvalidSdJwtVc = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMn0.TsFJUFKwdw5kVL4eY5vHOPGHqXBCFJ-n9c9KwPHkXAVfZ1TZkGA8m0_sNuTDy5n_pCutd6uzKJDAM0dfeGPyDg~' + +/**simpleSdJwtVcWithStatus + { + "jwt": { + "header": { + "typ": "vc+sd-jwt", + "alg": "EdDSA", + "kid": "#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW" + }, + "payload": { + "claim": "some-claim", + "vct": "IdentityCredential", + "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", + "iat": 1698151532, + "status": { + "status_list": { + "idx": 12, + "uri": "https://example.com/status-list" + } + } + }, + "signature": "vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg" + }, + "disclosures": [] + } + */ +export const simpleSdJwtVcWithStatus = + 'eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFZERTQSIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4IjoxMiwidXJpIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zdGF0dXMtbGlzdCJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.JWE6RRGt032UsQ9EoyJnvxq7dAQX2DjW6mLYuvDkCuq0fzse5V_7RO6R0RBCPHXWWIfnCNAA8oEI3QM6A3avDg~' + /**simpleJwtVcWithoutHolderBinding { "jwt": { @@ -39,7 +79,7 @@ export const simpleJwtVc = "claim": "some-claim", "vct": "IdentityCredential", "iss": "did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW", - "iat": 1698151532 + "iat": 1698151532, }, "signature": "vLkigrBr1IIVRJeYE5DQx0rKUVzO3KT9T0XBATWJE89pWCyvB3Rzs8VD7qfi0vDk_QVCPIiHq1U1PsmSe4ZqCg" }, diff --git a/packages/core/src/utils/fetch.ts b/packages/core/src/utils/fetch.ts new file mode 100644 index 0000000000..1c2f842de3 --- /dev/null +++ b/packages/core/src/utils/fetch.ts @@ -0,0 +1,28 @@ +import type { AgentDependencies } from '../agent/AgentDependencies' + +import { AbortController } from 'abort-controller' + +export async function fetchWithTimeout( + fetch: AgentDependencies['fetch'], + url: string, + init?: Omit & { + /** + * @default 5000 + */ + timeoutMs?: number + } +) { + const abortController = new AbortController() + const timeoutMs = init?.timeoutMs ?? 5000 + + const timeout = setTimeout(() => abortController.abort(), timeoutMs) + + try { + return await fetch(url, { + ...init, + signal: abortController.signal as NonNullable, + }) + } finally { + clearTimeout(timeout) + } +} diff --git a/yarn.lock b/yarn.lock index 7e3cc04ff9..73fb80bbfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2373,15 +2373,15 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== -"@sd-jwt/core@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.6.1.tgz#d28be10d0f4b672636fcf7ad71737cb08e5dae96" - integrity sha512-egFTb23o6BGWF93vnjopN02rSiC1HOOnkk9BI8Kao3jz9ipZAHdO6wF7gwfZm5Nlol/Kd1/KSLhbOUPYt++FjA== +"@sd-jwt/core@0.7.0", "@sd-jwt/core@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.7.0.tgz#ebacddd4015db24c8901a06bb3b771eddea28d1d" + integrity sha512-S6syiR6KUfQMWB1YlBudB9iNIji0p+Ip2XSPdqBkSCBJq7o5RBGhoZppHjuOFe+5KImQYv1ltEyJtNZqiRU/rQ== dependencies: - "@sd-jwt/decode" "0.6.1" - "@sd-jwt/present" "0.6.1" - "@sd-jwt/types" "0.6.1" - "@sd-jwt/utils" "0.6.1" + "@sd-jwt/decode" "0.7.0" + "@sd-jwt/present" "0.7.0" + "@sd-jwt/types" "0.7.0" + "@sd-jwt/utils" "0.7.0" "@sd-jwt/decode@0.6.1", "@sd-jwt/decode@^0.6.1": version "0.6.1" @@ -2391,6 +2391,14 @@ "@sd-jwt/types" "0.6.1" "@sd-jwt/utils" "0.6.1" +"@sd-jwt/decode@0.7.0", "@sd-jwt/decode@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.7.0.tgz#b899edba3582b4b6db2e90cf010c6aebad9a360c" + integrity sha512-XPLwelf8pOVWsJ9D+Y0BW6nvKN7Q/+cTcSRBlFcGYh/vJ4MGpT4Q1c4V6RrawpI9UUE485OqG/xhftCJ+x6P2Q== + dependencies: + "@sd-jwt/types" "0.7.0" + "@sd-jwt/utils" "0.7.0" + "@sd-jwt/decode@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.2.0.tgz#44211418fd0884a160f8223feedfe04ae52398c4" @@ -2399,7 +2407,25 @@ "@sd-jwt/types" "0.2.0" "@sd-jwt/utils" "0.2.0" -"@sd-jwt/present@0.6.1", "@sd-jwt/present@^0.6.1": +"@sd-jwt/jwt-status-list@0.7.0", "@sd-jwt/jwt-status-list@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/jwt-status-list/-/jwt-status-list-0.7.0.tgz#80d8ebb924c7e628927eaebd8f6ccaf61f80b27a" + integrity sha512-2cRiay88u+yLqO17oaoRlVR7amYo2RFWKYpvThsbCH5K9HrVBOZXBzJYZs771gXqeezcFNlFy9xbsqKJ9NqrUw== + dependencies: + "@sd-jwt/types" "0.7.0" + base64url "^3.0.1" + pako "^2.1.0" + +"@sd-jwt/present@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.7.0.tgz#a9c7494a1962a48e94953e78bd1cd28d1f7ef351" + integrity sha512-ozWRrobhmfSmAC2v0D6oS+abPPcmAjxGPZsaJL/064n1CN9rkbsRmyZvZ8U7rADJYF6INqE295Zpz1p6MaYK+w== + dependencies: + "@sd-jwt/decode" "0.7.0" + "@sd-jwt/types" "0.7.0" + "@sd-jwt/utils" "0.7.0" + +"@sd-jwt/present@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.6.1.tgz#82b9188becb0fa240897c397d84a54d55c7d169e" integrity sha512-QRD3TUDLj4PqQNZ70bBxh8FLLrOE9mY8V9qiZrJSsaDOLFs2p1CtZG+v9ig62fxFYJZMf4bWKwYjz+qqGAtxCg== @@ -2408,6 +2434,14 @@ "@sd-jwt/types" "0.6.1" "@sd-jwt/utils" "0.6.1" +"@sd-jwt/sd-jwt-vc@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/sd-jwt-vc/-/sd-jwt-vc-0.7.0.tgz#48c572881f5908f45b80e235bb970a6b4aa2f5eb" + integrity sha512-7MkCKY8Ittqvd7HJT+Ebge2d1tS7Y7xd+jmZFJJ5h6tzXlIPz08X1UzxR6f8h97euqCCDDCLJWfG34HKHE4lYw== + dependencies: + "@sd-jwt/core" "0.7.0" + "@sd-jwt/jwt-status-list" "0.7.0" + "@sd-jwt/types@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.2.0.tgz#3cb50392e1b76ce69453f403c71c937a6e202352" @@ -2418,6 +2452,11 @@ resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.6.1.tgz#fc4235e00cf40d35a21d6bc02e44e12d7162aa9b" integrity sha512-LKpABZJGT77jNhOLvAHIkNNmGqXzyfwBT+6r+DN9zNzMx1CzuNR0qXk1GMUbast9iCfPkGbnEpUv/jHTBvlIvg== +"@sd-jwt/types@0.7.0", "@sd-jwt/types@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.7.0.tgz#28fb3a9772467e771dc214112ef66362162c4528" + integrity sha512-jSHokgiv2rR/6bORK1Ym7WZb4k9mImVneE/Lt0rqKTA7xSX5IwIOCpjEAG7dIRWUqIdC1pFPRI/XkGri2BUiPQ== + "@sd-jwt/utils@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.2.0.tgz#ef52b744116e874f72ec01978f0631ad5a131eb7" @@ -2426,7 +2465,7 @@ "@sd-jwt/types" "0.2.0" buffer "*" -"@sd-jwt/utils@0.6.1", "@sd-jwt/utils@^0.6.1": +"@sd-jwt/utils@0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.6.1.tgz#33273b20c9eb1954e4eab34118158b646b574ff9" integrity sha512-1NHZ//+GecGQJb+gSdDicnrHG0DvACUk9jTnXA5yLZhlRjgkjyfJLNsCZesYeCyVp/SiyvIC9B+JwoY4kI0TwQ== @@ -2434,6 +2473,14 @@ "@sd-jwt/types" "0.6.1" js-base64 "^3.7.6" +"@sd-jwt/utils@0.7.0", "@sd-jwt/utils@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.7.0.tgz#382abc116b70c5ab83726de5871eda304499621f" + integrity sha512-/paioVASVMuNA2YOf/Bn9eGCot5QXBsMSYJNf7kcC+9UHsTgI2emVP2oYlBQRJHVduohy0BuaqpsOHDpZwoE0A== + dependencies: + "@sd-jwt/types" "0.7.0" + js-base64 "^3.7.6" + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" @@ -9590,7 +9637,7 @@ pacote@^15.0.0, pacote@^15.0.8: ssri "^10.0.0" tar "^6.1.11" -pako@^2.0.4: +pako@^2.0.4, pako@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== From aa4e6c814da3ef6f7f3dc42e323ce0cd1c3fc4e2 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 19 May 2024 18:54:49 +0800 Subject: [PATCH 2/5] remove log Signed-off-by: Timo Glastra --- packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts index 23c29a7c16..91d7f8197d 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -99,7 +99,6 @@ export class SdJwtVcService { if (!payload.vct || typeof payload.vct !== 'string') { throw new SdJwtVcError("Missing required parameter 'vct'") } - console.log(payload) const compact = await sdjwt.issue( { From 7f0a8c80224264e57ac19b5306cf0bb0ff68e964 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 19 May 2024 19:48:22 +0800 Subject: [PATCH 3/5] fix Signed-off-by: Timo Glastra --- .../src/openid4vc-holder/OpenId4VciHolderService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index 6c25997df5..d9fe77d11f 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -526,16 +526,16 @@ export class OpenId4VciHolderService { ) const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) - const { verification, sdJwtVc } = await sdJwtVcApi.verify({ + const verificationResult = await sdJwtVcApi.verify({ compactSdJwtVc: credentialResponse.successBody.credential, }) - if (!verification.isValid) { - agentContext.config.logger.error('Failed to validate credential', { verification }) - throw new CredoError(`Failed to validate sd-jwt-vc credential. Results = ${JSON.stringify(verification)}`) + if (!verificationResult.isValid) { + agentContext.config.logger.error('Failed to validate credential', { verificationResult }) + throw new CredoError(`Failed to validate sd-jwt-vc credential. Results = ${JSON.stringify(verificationResult)}`) } - return sdJwtVc + return verificationResult.sdJwtVc } else if ( format === OpenId4VciCredentialFormatProfile.JwtVcJson || format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd From acd1a769668ee3e195feaac5d5d1e2e0d5c715fd Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 4 Jun 2024 17:46:26 +0800 Subject: [PATCH 4/5] chore: address feedback Signed-off-by: Timo Glastra --- .../core/src/modules/sd-jwt-vc/SdJwtVcService.ts | 15 +++++++++------ .../sd-jwt-vc/__tests__/SdJwtVcService.test.ts | 16 ---------------- packages/core/src/utils/index.ts | 1 + packages/core/src/utils/timestamp.ts | 7 +++++++ 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts index 91d7f8197d..f4a054e536 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -19,13 +19,16 @@ import { uint8ArrayToBase64Url } from '@sd-jwt/utils' import { injectable } from 'tsyringe' import { JwtPayload, Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto' -import { TypedArrayEncoder, Hasher } from '../../utils' +import { CredoError } from '../../error' +import { TypedArrayEncoder, Hasher, nowInSeconds } from '../../utils' import { fetchWithTimeout } from '../../utils/fetch' import { DidResolverService, parseDid, getKeyFromVerificationMethod } from '../dids' import { SdJwtVcError } from './SdJwtVcError' import { SdJwtVcRecord, SdJwtVcRepository } from './repository' +type SdJwtVcConfig = SDJwtVcInstance['userConfig'] + export interface SdJwtVc< Header extends SdJwtVcHeader = SdJwtVcHeader, Payload extends SdJwtVcPayload = SdJwtVcPayload @@ -93,7 +96,6 @@ export class SdJwtVcService { signer: this.signer(agentContext, issuer.key), hashAlg: 'sha-256', signAlg: issuer.alg, - saltGenerator: agentContext.wallet.generateNonce, }) if (!payload.vct || typeof payload.vct !== 'string') { @@ -105,7 +107,7 @@ export class SdJwtVcService { ...payload, cnf: holderBinding?.cnf, iss: issuer.iss, - iat: Math.floor(new Date().getTime() / 1000), + iat: nowInSeconds(), vct: payload.vct, }, disclosureFrame as DisclosureFrame, @@ -193,7 +195,7 @@ export class SdJwtVcService { try { sdJwtVc = await sdjwt.decode(compactSdJwtVc) - if (!sdJwtVc.jwt) throw new Error('Invalid sd-jwt-vc') + if (!sdJwtVc.jwt) throw new CredoError('Invalid sd-jwt-vc') } catch (error) { return { isValid: false, @@ -512,10 +514,11 @@ export class SdJwtVcService { throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.") } - private getBaseSdJwtConfig(agentContext: AgentContext) { + private getBaseSdJwtConfig(agentContext: AgentContext): SdJwtVcConfig { return { hasher: this.hasher, statusListFetcher: this.getStatusListFetcher(agentContext), + saltGenerator: agentContext.wallet.generateNonce, } } @@ -527,7 +530,7 @@ export class SdJwtVcService { return async (uri: string) => { const response = await fetchWithTimeout(agentContext.config.agentDependencies.fetch, uri) if (!response.ok) { - throw new Error( + throw new CredoError( `Received invalid response with status ${ response.status } when fetching status list from ${uri}. ${await response.text()}` diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts index 7751e1cac2..08b392123f 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts @@ -753,22 +753,6 @@ describe('SdJwtVcService', () => { test('Verify sd-jwt-vc with status where credential is not revoked', async () => { const sdJwtVcService = agent.dependencyManager.resolve(SdJwtVcService) - // const { compact } = await sdJwtVcService.sign(agent.context, { - // payload: { - // claim: 'some-claim', - // vct: 'IdentityCredential', - // status: { - // status_list: { - // idx: 12, - // uri: 'https://example.com/status-list', - // }, - // }, - // }, - // issuer: { - // method: 'did', - // didUrl: issuerDidUrl, - // }, - // }) // Mock call to status list const fetchSpy = jest.spyOn(fetchUtils, 'fetchWithTimeout') diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index d5034609ae..42cb459fba 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -14,4 +14,5 @@ export * from './objectEquality' export * from './MessageValidator' export * from './did' export * from './array' +export * from './timestamp' export { DateTransformer } from './transformers' diff --git a/packages/core/src/utils/timestamp.ts b/packages/core/src/utils/timestamp.ts index 0ad7dd57eb..4798fcc24f 100644 --- a/packages/core/src/utils/timestamp.ts +++ b/packages/core/src/utils/timestamp.ts @@ -10,3 +10,10 @@ export default function timestamp(): Uint8Array { } return Uint8Array.from(bytes).reverse() } + +/** + * Returns the current time in seconds + */ +export function nowInSeconds() { + return Math.floor(new Date().getTime() / 1000) +} From 9c0f46b545fe9b380f936767b7bb0876a0cfa190 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 4 Jun 2024 23:13:42 +0800 Subject: [PATCH 5/5] merge Signed-off-by: Timo Glastra --- .github/workflows/continuous-deployment.yml | 4 +- .github/workflows/lint-pr.yml | 2 +- Dockerfile | 2 +- network/indy-pool.dockerfile | 51 +---- .../AnonCredsCredentialFormatService.ts | 14 +- .../LegacyIndyCredentialFormatService.ts | 14 +- .../credentials/v1/V1CredentialProtocol.ts | 17 +- .../V1CredentialProtocolCred.test.ts | 1 - .../protocols/proofs/v1/V1ProofProtocol.ts | 15 +- .../v1/__tests__/V1ProofProtocol.test.ts | 1 - .../0.4-0.5/anonCredsCredentialRecord.ts | 11 ++ .../anoncreds/src/utils/indyIdentifiers.ts | 2 + .../anoncreds/src/utils/w3cAnonCredsUtils.ts | 3 +- .../__tests__/ConnectionService.test.ts | 29 +++ .../connections/services/ConnectionService.ts | 15 +- .../protocol/BaseCredentialProtocol.ts | 20 +- .../services/RevocationNotificationService.ts | 16 +- .../RevocationNotificationService.test.ts | 2 +- .../protocol/v2/V2CredentialProtocol.ts | 5 + .../V2CredentialProtocolCred.test.ts | 1 - .../proofs/protocol/BaseProofProtocol.ts | 18 +- .../proofs/protocol/v2/V2ProofProtocol.ts | 4 + .../v2/__tests__/V2ProofProtocol.test.ts | 1 - .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 8 +- .../indy-vdr-anoncreds-registry.e2e.test.ts | 177 +++++++++++++++++- packages/openid4vc/package.json | 6 +- yarn.lock | 110 +++-------- 27 files changed, 370 insertions(+), 179 deletions(-) diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index b9e47524ab..77a2d54e22 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -84,13 +84,13 @@ jobs: echo version="${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Create Tag - uses: mathieudutour/github-tag-action@v6.1 + uses: mathieudutour/github-tag-action@v6.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} custom_tag: ${{ steps.new-version.outputs.version }} - name: Create Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.new-version.outputs.version }} body: | diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 50b29cb22e..06d8acabcd 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -14,7 +14,7 @@ jobs: steps: # Please look up the latest version from # https://github.com/amannn/action-semantic-pull-request/releases - - uses: amannn/action-semantic-pull-request@v5.4.0 + - uses: amannn/action-semantic-pull-request@v5.5.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/Dockerfile b/Dockerfile index af0b50d8e4..601bb9545b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:21 +FROM node:22 # Set working directory WORKDIR /www diff --git a/network/indy-pool.dockerfile b/network/indy-pool.dockerfile index 1352f62163..8705c8a79d 100644 --- a/network/indy-pool.dockerfile +++ b/network/indy-pool.dockerfile @@ -1,48 +1,9 @@ -FROM ubuntu:16.04 +FROM bcgovimages/von-image:node-1.12-6 -ARG uid=1000 +USER root # Install environment -RUN apt-get update -y && apt-get install -y \ - git \ - wget \ - python3.5 \ - python3-pip \ - python-setuptools \ - python3-nacl \ - apt-transport-https \ - ca-certificates \ - supervisor \ - gettext-base \ - software-properties-common - -RUN pip3 install -U \ - pip==9.0.3 \ - setuptools - -RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 || \ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys CE7709D068DB5E88 -ARG indy_stream=stable -RUN echo "deb https://repo.sovrin.org/deb xenial $indy_stream" >> /etc/apt/sources.list -RUN add-apt-repository "deb https://repo.sovrin.org/sdk/deb xenial $indy_stream" - -RUN useradd -ms /bin/bash -u $uid indy - -ARG indy_plenum_ver=1.12.1 -ARG indy_node_ver=1.12.1 -ARG python3_indy_crypto_ver=0.4.5 -ARG indy_crypto_ver=0.4.5 -ARG python3_pyzmq_ver=18.1.0 - -RUN apt-get update -y && apt-get install -y \ - python3-pyzmq=${python3_pyzmq_ver} \ - indy-plenum=${indy_plenum_ver} \ - indy-node=${indy_node_ver} \ - python3-indy-crypto=${python3_indy_crypto_ver} \ - libindy-crypto=${indy_crypto_ver} \ - vim \ - libindy \ - indy-cli +RUN apt-get update -y && apt-get install -y supervisor # It is imporatnt the the lines are not indented. Some autformatters # Indent the supervisord parameters. THIS WILL BREAK THE SETUP @@ -90,11 +51,9 @@ stderr_logfile=/tmp/node4.log\n"\ USER indy -RUN awk '{if (index($1, "NETWORK_NAME") != 0) {print("NETWORK_NAME = \"sandbox\"")} else print($0)}' /etc/indy/indy_config.py> /tmp/indy_config.py -RUN mv /tmp/indy_config.py /etc/indy/indy_config.py +COPY --chown=indy:indy network/indy_config.py /etc/indy/indy_config.py ARG pool_ip=127.0.0.1 - RUN generate_indy_pool_transactions --nodes 4 --clients 5 --nodeNum 1 2 3 4 --ips="$pool_ip,$pool_ip,$pool_ip,$pool_ip" COPY network/add-did.sh /usr/bin/add-did @@ -105,4 +64,4 @@ COPY network/indy-cli-config.json /etc/indy/indy-cli-config.json EXPOSE 9701 9702 9703 9704 9705 9706 9707 9708 -CMD ["/usr/bin/supervisord"] +CMD ["/usr/bin/supervisord"] \ No newline at end of file diff --git a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts index efebabc26e..0d308f87b2 100644 --- a/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsCredentialFormatService.ts @@ -341,7 +341,7 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService revocationStatusList = revocationStatusListResult.revocationStatusList } - const { credential } = await anonCredsIssuerService.createCredential(agentContext, { + const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer, credentialRequest, credentialValues: convertAttributesToCredentialValues(credentialAttributes), @@ -350,6 +350,18 @@ export class AnonCredsCredentialFormatService implements CredentialFormatService revocationStatusList, }) + // If the credential is revocable, store the revocation identifiers in the credential record + if (credential.rev_reg_id) { + credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { + revocationRegistryId: revocationRegistryDefinitionId ?? undefined, + credentialRevocationId: credentialRevocationId ?? undefined, + }) + credentialRecord.setTags({ + anonCredsRevocationRegistryId: revocationRegistryDefinitionId, + anonCredsCredentialRevocationId: credentialRevocationId, + }) + } + const format = new CredentialFormatSpec({ attachmentId, format: ANONCREDS_CREDENTIAL, diff --git a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts index 5a2d7b9592..07ead68328 100644 --- a/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts +++ b/packages/anoncreds/src/formats/LegacyIndyCredentialFormatService.ts @@ -300,23 +300,12 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic const credentialRequest = requestAttachment.getDataAsJson() if (!credentialRequest) throw new CredoError('Missing indy credential request in createCredential') - const { credential, credentialRevocationId } = await anonCredsIssuerService.createCredential(agentContext, { + const { credential } = await anonCredsIssuerService.createCredential(agentContext, { credentialOffer, credentialRequest, credentialValues: convertAttributesToCredentialValues(credentialAttributes), }) - if (credential.rev_reg_id) { - credentialRecord.metadata.add(AnonCredsCredentialMetadataKey, { - credentialRevocationId: credentialRevocationId, - revocationRegistryId: credential.rev_reg_id, - }) - credentialRecord.setTags({ - anonCredsRevocationRegistryId: credential.rev_reg_id, - anonCredsCredentialRevocationId: credentialRevocationId, - }) - } - const format = new CredentialFormatSpec({ attachmentId, format: INDY_CRED, @@ -399,6 +388,7 @@ export class LegacyIndyCredentialFormatService implements CredentialFormatServic }) credentialRecord.setTags({ anonCredsRevocationRegistryId: credential.revocationRegistryId, + anonCredsUnqualifiedRevocationRegistryId: anonCredsCredential.rev_reg_id, anonCredsCredentialRevocationId: credential.credentialRevocationId, }) } diff --git a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts index ce3a306704..1937a7c20c 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts @@ -231,6 +231,7 @@ export class V1CredentialProtocol await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage, lastSentMessage, + expectedConnectionId: credentialRecord.connectionId, }) await this.indyCredentialFormat.processProposal(messageContext.agentContext, { @@ -251,6 +252,8 @@ export class V1CredentialProtocol }) } else { agentContext.config.logger.debug('Credential record does not exists yet for incoming proposal') + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) // No credential record exists with thread id credentialRecord = new CredentialExchangeRecord({ @@ -261,9 +264,6 @@ export class V1CredentialProtocol protocolVersion: 'v1', }) - // Assert - await connectionService.assertConnectionOrOutOfBandExchange(messageContext) - // Save record await credentialRepository.save(messageContext.agentContext, credentialRecord) this.emitStateChangedEvent(messageContext.agentContext, credentialRecord, null) @@ -532,6 +532,7 @@ export class V1CredentialProtocol await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage, lastSentMessage, + expectedConnectionId: credentialRecord.connectionId, }) await this.indyCredentialFormat.processOffer(messageContext.agentContext, { @@ -548,6 +549,9 @@ export class V1CredentialProtocol return credentialRecord } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + // No credential record exists with thread id credentialRecord = new CredentialExchangeRecord({ connectionId: connection?.id, @@ -558,9 +562,6 @@ export class V1CredentialProtocol protocolVersion: 'v1', }) - // Assert - await connectionService.assertConnectionOrOutOfBandExchange(messageContext) - await this.indyCredentialFormat.processOffer(messageContext.agentContext, { credentialRecord, attachment: offerAttachment, @@ -767,6 +768,7 @@ export class V1CredentialProtocol await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage: proposalMessage ?? undefined, lastSentMessage: offerMessage ?? undefined, + expectedConnectionId: credentialRecord.connectionId, }) // This makes sure that the sender of the incoming message is authorized to do so. @@ -774,7 +776,6 @@ export class V1CredentialProtocol await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { expectedConnectionId: credentialRecord.connectionId, }) - credentialRecord.connectionId = connection?.id } @@ -916,6 +917,7 @@ export class V1CredentialProtocol await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage: offerCredentialMessage, lastSentMessage: requestCredentialMessage, + expectedConnectionId: credentialRecord.connectionId, }) const issueAttachment = issueMessage.getCredentialAttachmentById(INDY_CREDENTIAL_ATTACHMENT_ID) @@ -1022,6 +1024,7 @@ export class V1CredentialProtocol await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage: requestCredentialMessage, lastSentMessage: issueCredentialMessage, + expectedConnectionId: credentialRecord.connectionId, }) // Update record diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts index 8912207417..c2d9e81203 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts @@ -736,7 +736,6 @@ describe('V1CredentialProtocol', () => { } expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { threadId: 'somethreadid', - connectionId: connection.id, }) expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) const [[, updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls diff --git a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts index dafb943139..df5c931712 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts @@ -198,6 +198,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage, lastSentMessage, + expectedConnectionId: proofRecord.connectionId, }) // Update record @@ -209,6 +210,8 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< await this.updateState(agentContext, proofRecord, ProofState.ProposalReceived) } else { agentContext.config.logger.debug('Proof record does not exist yet for incoming proposal') + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) // No proof record exists with thread id proofRecord = new ProofExchangeRecord({ @@ -220,9 +223,6 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< protocolVersion: 'v1', }) - // Assert - await connectionService.assertConnectionOrOutOfBandExchange(messageContext) - await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { agentMessage: proposalMessage, associatedRecordId: proofRecord.id, @@ -456,6 +456,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage, lastSentMessage, + expectedConnectionId: proofRecord.connectionId, }) await this.indyProofFormat.processRequest(agentContext, { @@ -470,6 +471,9 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< }) await this.updateState(agentContext, proofRecord, ProofState.RequestReceived) } else { + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) + // No proof record exists with thread id proofRecord = new ProofExchangeRecord({ connectionId: connection?.id, @@ -491,9 +495,6 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< role: DidCommMessageRole.Receiver, }) - // Assert - await connectionService.assertConnectionOrOutOfBandExchange(messageContext) - // Save in repository await proofRepository.save(agentContext, proofRecord) this.emitStateChangedEvent(agentContext, proofRecord, null) @@ -791,6 +792,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage: proposalMessage, lastSentMessage: requestMessage, + expectedConnectionId: proofRecord.connectionId, }) // This makes sure that the sender of the incoming message is authorized to do so. @@ -922,6 +924,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { lastReceivedMessage, lastSentMessage, + expectedConnectionId: proofRecord.connectionId, }) // Update record diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/V1ProofProtocol.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/V1ProofProtocol.test.ts index 46ae4cfc1b..5d7a2cde62 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/V1ProofProtocol.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/V1ProofProtocol.test.ts @@ -245,7 +245,6 @@ describe('V1ProofProtocol', () => { } expect(proofRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { threadId: 'somethreadid', - connectionId: connection.id, }) expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) const [[, updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls diff --git a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts index c55972500d..6c2791ca40 100644 --- a/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts +++ b/packages/anoncreds/src/updates/0.4-0.5/anonCredsCredentialRecord.ts @@ -16,6 +16,7 @@ import { fetchCredentialDefinition } from '../../utils/anonCredsObjects' import { getIndyNamespaceFromIndyDid, getQualifiedDidIndyDid, + getUnQualifiedDidIndyDid, isIndyDid, isUnqualifiedCredentialDefinitionId, isUnqualifiedIndyDid, @@ -154,6 +155,16 @@ async function migrateLegacyToW3cCredential(agentContext: AgentContext, legacyRe credentialRecordType: 'w3c', credentialRecordId: w3cCredentialRecord.id, } + + // If using unqualified dids, store both qualified/unqualified revRegId forms + // to allow retrieving it from revocation notification service + if (legacyTags.revocationRegistryId && indyNamespace) { + relatedCredentialExchangeRecord.setTags({ + anonCredsRevocationRegistryId: getQualifiedDidIndyDid(legacyTags.revocationRegistryId, indyNamespace), + anonCredsUnqualifiedRevocationRegistryId: getUnQualifiedDidIndyDid(legacyTags.revocationRegistryId), + }) + } + await credentialExchangeRepository.update(agentContext, relatedCredentialExchangeRecord) } } diff --git a/packages/anoncreds/src/utils/indyIdentifiers.ts b/packages/anoncreds/src/utils/indyIdentifiers.ts index 6b957bfc5d..c31d26b03e 100644 --- a/packages/anoncreds/src/utils/indyIdentifiers.ts +++ b/packages/anoncreds/src/utils/indyIdentifiers.ts @@ -221,6 +221,8 @@ export function getIndyNamespaceFromIndyDid(identifier: string): string { } export function getUnQualifiedDidIndyDid(identifier: string): string { + if (isUnqualifiedIndyDid(identifier)) return identifier + if (isDidIndySchemaId(identifier)) { const { schemaName, schemaVersion, namespaceIdentifier } = parseIndySchemaId(identifier) return getUnqualifiedSchemaId(namespaceIdentifier, schemaName, schemaVersion) diff --git a/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts b/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts index fe9bfa0c58..550a97c3f3 100644 --- a/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts +++ b/packages/anoncreds/src/utils/w3cAnonCredsUtils.ts @@ -20,6 +20,7 @@ import { isUnqualifiedRevocationRegistryId, isIndyDid, getUnQualifiedDidIndyDid, + isUnqualifiedIndyDid, } from './indyIdentifiers' import { W3cAnonCredsCredentialMetadataKey } from './metadata' @@ -199,7 +200,7 @@ export function getW3cRecordAnonCredsTags(options: { anonCredsMethodName: methodName, anonCredsRevocationRegistryId: revocationRegistryId, anonCredsCredentialRevocationId: credentialRevocationId, - ...(isIndyDid(issuerId) && { + ...((isIndyDid(issuerId) || isUnqualifiedIndyDid(issuerId)) && { anonCredsUnqualifiedIssuerId: getUnQualifiedDidIndyDid(issuerId), anonCredsUnqualifiedCredentialDefinitionId: getUnQualifiedDidIndyDid(credentialDefinitionId), anonCredsUnqualifiedSchemaId: getUnQualifiedDidIndyDid(schemaId), diff --git a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts index 0f2d58ff05..24256e561e 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts @@ -753,6 +753,35 @@ describe('ConnectionService', () => { }) describe('assertConnectionOrOutOfBandExchange', () => { + it('should throw an error when a expectedConnectionId is present, but no connection is present in the messageContext', async () => { + expect.assertions(1) + + const messageContext = new InboundMessageContext(new AgentMessage(), { + agentContext, + }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + expectedConnectionId: '123', + }) + ).rejects.toThrow('Expected incoming message to be from connection 123 but no connection found.') + }) + + it('should throw an error when a expectedConnectionId is present, but does not match with connection id present in the messageContext', async () => { + expect.assertions(1) + + const messageContext = new InboundMessageContext(new AgentMessage(), { + agentContext, + connection: getMockConnection({ state: DidExchangeState.InvitationReceived, id: 'something' }), + }) + + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + expectedConnectionId: 'something-else', + }) + ).rejects.toThrow('Expected incoming message to be from connection something-else but connection is something.') + }) + it('should not throw an error when a connection record with state complete is present in the messageContext', async () => { expect.assertions(1) diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index f823ade7c0..34e06982f1 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -459,13 +459,26 @@ export class ConnectionService { { lastSentMessage, lastReceivedMessage, + expectedConnectionId, }: { lastSentMessage?: AgentMessage | null lastReceivedMessage?: AgentMessage | null + expectedConnectionId?: string } = {} ) { const { connection, message } = messageContext + if (expectedConnectionId && !connection) { + throw new CredoError( + `Expected incoming message to be from connection ${expectedConnectionId} but no connection found.` + ) + } + if (expectedConnectionId && connection?.id !== expectedConnectionId) { + throw new CredoError( + `Expected incoming message to be from connection ${expectedConnectionId} but connection is ${connection?.id}.` + ) + } + // Check if we have a ready connection. Verification is already done somewhere else. Return if (connection) { connection.assertReady() @@ -559,7 +572,7 @@ export class ConnectionService { messageContext: InboundMessageContext, { expectedConnectionId }: { expectedConnectionId?: string } ) { - if (expectedConnectionId && messageContext.connection?.id === expectedConnectionId) { + if (expectedConnectionId && messageContext.connection?.id !== expectedConnectionId) { throw new CredoError( `Expecting incoming message to have connection ${expectedConnectionId}, but incoming connection is ${ messageContext.connection?.id ?? 'undefined' diff --git a/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts index 6da4bdc9fb..5e9259ee20 100644 --- a/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/BaseCredentialProtocol.ts @@ -28,6 +28,7 @@ import type { CredentialExchangeRecord } from '../repository' import { EventEmitter } from '../../../agent/EventEmitter' import { DidCommMessageRepository } from '../../../storage/didcomm' +import { ConnectionService } from '../../connections' import { CredentialEventTypes } from '../CredentialEvents' import { CredentialState } from '../models/CredentialState' import { CredentialRepository } from '../repository' @@ -136,17 +137,30 @@ export abstract class BaseCredentialProtocol ): Promise { - const { message: credentialProblemReportMessage, agentContext } = messageContext + const { message: credentialProblemReportMessage, agentContext, connection } = messageContext - const connection = messageContext.assertReadyConnection() + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) agentContext.config.logger.debug(`Processing problem report with message id ${credentialProblemReportMessage.id}`) const credentialRecord = await this.getByProperties(agentContext, { threadId: credentialProblemReportMessage.threadId, - connectionId: connection.id, }) + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + expectedConnectionId: credentialRecord.connectionId, + }) + + // This makes sure that the sender of the incoming message is authorized to do so. + if (!credentialRecord?.connectionId) { + await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { + expectedConnectionId: credentialRecord?.connectionId, + }) + + credentialRecord.connectionId = connection?.id + } + // Update record credentialRecord.errorMessage = `${credentialProblemReportMessage.description.code}: ${credentialProblemReportMessage.description.en}` await this.updateState(agentContext, credentialRecord, CredentialState.Abandoned) diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts index 97b2afec14..ff0a1dd4b0 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/RevocationNotificationService.ts @@ -51,7 +51,21 @@ export class RevocationNotificationService { comment?: string ) { // TODO: can we extract support for this revocation notification handler to the anoncreds module? - const query = { anonCredsRevocationRegistryId, anonCredsCredentialRevocationId, connectionId: connection.id } + // Search for the revocation registry in both qualified and unqualified forms + const query = { + $or: [ + { + anonCredsRevocationRegistryId, + anonCredsCredentialRevocationId, + connectionId: connection.id, + }, + { + anonCredsUnqualifiedRevocationRegistryId: anonCredsRevocationRegistryId, + anonCredsCredentialRevocationId, + connectionId: connection.id, + }, + ], + } this.logger.trace(`Getting record by query for revocation notification:`, query) const credentialRecord = await this.credentialRepository.getSingleByQuery(agentContext, query) diff --git a/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts b/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts index abd50e160b..d8c545a0fc 100644 --- a/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts +++ b/packages/core/src/modules/credentials/protocol/revocation-notification/services/__tests__/RevocationNotificationService.test.ts @@ -78,7 +78,7 @@ describe('RevocationNotificationService', () => { } satisfies AnonCredsCredentialMetadata // Set required tags - credentialRecord.setTag('anonCredsRevocationRegistryId', metadata.revocationRegistryId) + credentialRecord.setTag('anonCredsUnqualifiedRevocationRegistryId', metadata.revocationRegistryId) credentialRecord.setTag('anonCredsCredentialRevocationId', metadata.credentialRevocationId) mockFunction(credentialRepository.getSingleByQuery).mockResolvedValueOnce(credentialRecord) diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts index 65152d4ada..82b19132cb 100644 --- a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts @@ -204,6 +204,7 @@ export class V2CredentialProtocol { expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { threadId: 'somethreadid', - connectionId: '123', }) expect(credentialRepository.update).toHaveBeenCalled() expect(returnedCredentialRecord.errorMessage).toBe('issuance-abandoned: Indy error') diff --git a/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts b/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts index 6729e94796..fa146221ff 100644 --- a/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts +++ b/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts @@ -30,6 +30,7 @@ import type { ProofExchangeRecord } from '../repository' import { EventEmitter } from '../../../agent/EventEmitter' import { DidCommMessageRepository } from '../../../storage/didcomm' +import { ConnectionService } from '../../connections' import { ProofEventTypes } from '../ProofEvents' import { ProofState } from '../models/ProofState' import { ProofRepository } from '../repository' @@ -112,13 +113,28 @@ export abstract class BaseProofProtocol { const { message: proofProblemReportMessage, agentContext, connection } = messageContext + const connectionService = agentContext.dependencyManager.resolve(ConnectionService) + agentContext.config.logger.debug(`Processing problem report with message id ${proofProblemReportMessage.id}`) const proofRecord = await this.getByProperties(agentContext, { threadId: proofProblemReportMessage.threadId, - connectionId: connection?.id, }) + // Assert + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + expectedConnectionId: proofRecord.connectionId, + }) + + // This makes sure that the sender of the incoming message is authorized to do so. + if (!proofRecord?.connectionId) { + await connectionService.matchIncomingMessageToRequestMessageInOutOfBandExchange(messageContext, { + expectedConnectionId: proofRecord?.connectionId, + }) + + proofRecord.connectionId = connection?.id + } + // Update record proofRecord.errorMessage = `${proofProblemReportMessage.description.code}: ${proofProblemReportMessage.description.en}` await this.updateState(agentContext, proofRecord, ProofState.Abandoned) diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts index a84a2fe699..bc48bd8da7 100644 --- a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts +++ b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts @@ -192,6 +192,7 @@ export class V2ProofProtocol { } expect(proofRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, agentContext, { threadId: 'somethreadid', - connectionId: connection.id, }) expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) const [[, updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index e2e725dd71..b1c8841e8c 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -288,7 +288,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { // Format the schema id based on the type of the credential definition id const schemaId = credentialDefinitionId.startsWith('did:indy') - ? getDidIndySchemaId(pool.indyNamespace, namespaceIdentifier, schema.schema.name, schema.schema.version) + ? getDidIndySchemaId(pool.indyNamespace, schema.schema.issuerId, schema.schema.name, schema.schema.version) : schema.schema.schemaId return { @@ -979,8 +979,8 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } const schema = response.result.data?.txn.data as SchemaType - - const schemaId = getUnqualifiedSchemaId(did, schema.data.name, schema.data.version) + const schemaDid = response.result.data?.txn.metadata.from as string + const schemaId = getUnqualifiedSchemaId(schemaDid, schema.data.name, schema.data.version) return { schema: { @@ -988,7 +988,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { attr_name: schema.data.attr_names, name: schema.data.name, version: schema.data.version, - issuerId: did, + issuerId: schemaDid, seqNo, }, indyNamespace: pool.indyNamespace, diff --git a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts index 211066e7f6..d0647dc599 100644 --- a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts @@ -58,15 +58,20 @@ const indyVdrPoolService = endorser.dependencyManager.resolve(IndyVdrPoolService // FIXME: this test is very slow, probably due to the sleeps. Can we speed it up? describe('IndyVdrAnonCredsRegistry', () => { let endorserDid: string + let agentDid: string beforeAll(async () => { await endorser.initialize() + await agent.initialize() const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( endorser, TypedArrayEncoder.fromString('00000000000000000000000Endorser9') ) endorserDid = `did:indy:pool:localtest:${unqualifiedSubmitterDid}` - - await agent.initialize() + const agentUnqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( + agent, + TypedArrayEncoder.fromString('00000000000000000000000Endorser9') + ) + agentDid = `did:indy:pool:localtest:${agentUnqualifiedSubmitterDid}` }) afterAll(async () => { @@ -1153,4 +1158,172 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) }) + + test('register and resolve a credential definition (internal,credDefDid != schemaDid)', async () => { + const didCreateResult = (await endorser.dids.create({ + method: 'indy', + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + })) as IndyVdrDidCreateResult + + if (didCreateResult.didState.state !== 'finished') throw Error('did was not successfully created') + endorser.config.logger.debug(`didIndyIssuerId:: ${didCreateResult.didState.did}`) + const didIndyIssuerId = didCreateResult.didState.did + const { namespaceIdentifier: legacyIssuerId } = parseIndyDid(didIndyIssuerId) + const dynamicVersion = `1.${Math.random() * 100}` + const legacySchemaId = `${legacyIssuerId}:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + + const schemaResult = await indyVdrAnonCredsRegistry.registerSchema(endorser.context, { + options: { + endorserMode: 'internal', + endorserDid, + }, + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + }) + const schemaSeqNo = schemaResult.schemaMetadata.indyLedgerSeqNo as number + expect(schemaResult).toMatchObject({ + schemaState: { + state: 'finished', + schema: { + attrNames: ['age'], + issuerId: didIndyIssuerId, + name: 'test', + version: dynamicVersion, + }, + schemaId: didIndySchemaId, + }, + registrationMetadata: {}, + schemaMetadata: { + indyLedgerSeqNo: expect.any(Number), + }, + }) + // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + const legacySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, legacySchemaId) + expect(legacySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: legacyIssuerId, + }, + schemaId: legacySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + // Resolve using did indy schema id + const didIndySchema = await indyVdrAnonCredsRegistry.getSchema(endorser.context, didIndySchemaId) + expect(didIndySchema).toMatchObject({ + schema: { + attrNames: ['age'], + name: 'test', + version: dynamicVersion, + issuerId: didIndyIssuerId, + }, + schemaId: didIndySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + const agentDidCreateResult = (await agent.dids.create({ + method: 'indy', + options: { + endorserDid: agentDid, + endorserMode: 'internal', + }, + })) as IndyVdrDidCreateResult + + if (agentDidCreateResult.didState.state !== 'finished') throw Error('did was not successfully created') + const didIndyAgentIssuerId = agentDidCreateResult.didState.did + const { namespaceIdentifier: agentLegacyIssuerId } = parseIndyDid(didIndyAgentIssuerId) + + const legacyCredentialDefinitionId = `${agentLegacyIssuerId}:3:CL:${schemaSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${agentLegacyIssuerId}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/TAG` + + const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + credentialDefinition: { + issuerId: didIndyAgentIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + options: { + endorserMode: 'internal', + endorserDid: agentDid, + }, + }) + + expect(credentialDefinitionResult).toMatchObject({ + credentialDefinitionMetadata: {}, + credentialDefinitionState: { + credentialDefinition: { + issuerId: didIndyAgentIssuerId, + tag: 'TAG', + schemaId: didIndySchemaId, + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionId: didIndyCredentialDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // // Wait some time before resolving credential definition object + await new Promise((res) => setTimeout(res, 1000)) + const legacyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + legacyCredentialDefinitionId + ) + + expect(legacyCredentialDefinition).toMatchObject({ + credentialDefinitionId: legacyCredentialDefinitionId, + credentialDefinition: { + issuerId: agentLegacyIssuerId, + schemaId: legacySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + // resolve using did indy credential definition id + const didIndyCredentialDefinition = await indyVdrAnonCredsRegistry.getCredentialDefinition( + agent.context, + didIndyCredentialDefinitionId + ) + + expect(didIndyCredentialDefinition).toMatchObject({ + credentialDefinitionId: didIndyCredentialDefinitionId, + credentialDefinition: { + issuerId: didIndyAgentIssuerId, + schemaId: didIndySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + }) }) diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 8e05f271d5..eae4819a9f 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -26,9 +26,9 @@ "dependencies": { "@credo-ts/core": "0.5.3", "@sphereon/did-auth-siop": "^0.6.4", - "@sphereon/oid4vci-client": "^0.10.2", - "@sphereon/oid4vci-common": "^0.10.1", - "@sphereon/oid4vci-issuer": "^0.10.2", + "@sphereon/oid4vci-client": "^0.10.3", + "@sphereon/oid4vci-common": "^0.10.3", + "@sphereon/oid4vci-issuer": "^0.10.3", "@sphereon/ssi-types": "^0.23.0", "class-transformer": "^0.5.1", "rxjs": "^7.8.0" diff --git a/yarn.lock b/yarn.lock index 73fb80bbfe..7491d2f11a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2399,14 +2399,6 @@ "@sd-jwt/types" "0.7.0" "@sd-jwt/utils" "0.7.0" -"@sd-jwt/decode@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.2.0.tgz#44211418fd0884a160f8223feedfe04ae52398c4" - integrity sha512-nmiZN3SQ4ApapEu+rS1h/YAkDIq3exgN7swSCsEkrxSEwnBSbXtISIY/sv+EmwnehF1rcKbivHfHNxOWYtlxvg== - dependencies: - "@sd-jwt/types" "0.2.0" - "@sd-jwt/utils" "0.2.0" - "@sd-jwt/jwt-status-list@0.7.0", "@sd-jwt/jwt-status-list@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sd-jwt/jwt-status-list/-/jwt-status-list-0.7.0.tgz#80d8ebb924c7e628927eaebd8f6ccaf61f80b27a" @@ -2442,11 +2434,6 @@ "@sd-jwt/core" "0.7.0" "@sd-jwt/jwt-status-list" "0.7.0" -"@sd-jwt/types@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.2.0.tgz#3cb50392e1b76ce69453f403c71c937a6e202352" - integrity sha512-16WFRcL/maG0/JxN9UCSx07/vJ2SDbGscv9gDLmFLgJzhJcGPer41XfI6aDfVARYP430wHFixChfY/n7qC1L/Q== - "@sd-jwt/types@0.6.1", "@sd-jwt/types@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.6.1.tgz#fc4235e00cf40d35a21d6bc02e44e12d7162aa9b" @@ -2457,14 +2444,6 @@ resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.7.0.tgz#28fb3a9772467e771dc214112ef66362162c4528" integrity sha512-jSHokgiv2rR/6bORK1Ym7WZb4k9mImVneE/Lt0rqKTA7xSX5IwIOCpjEAG7dIRWUqIdC1pFPRI/XkGri2BUiPQ== -"@sd-jwt/utils@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.2.0.tgz#ef52b744116e874f72ec01978f0631ad5a131eb7" - integrity sha512-oHCfRYVHCb5RNwdq3eHAt7P9d7TsEaSM1TTux+xl1I9PeQGLtZETnto9Gchtzn8FlTrMdVsLlcuAcK6Viwj1Qw== - dependencies: - "@sd-jwt/types" "0.2.0" - buffer "*" - "@sd-jwt/utils@0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.6.1.tgz#33273b20c9eb1954e4eab34118158b646b574ff9" @@ -2581,34 +2560,34 @@ cross-fetch "^3.1.8" did-resolver "^4.1.0" -"@sphereon/oid4vci-client@^0.10.2": - version "0.10.2" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.10.2.tgz#70ceff97e6fb8220e8de5e626359ad2ea146ef1e" - integrity sha512-G0vE9/MwdyHQnYpnuaJqbRSIKXCLVyOVhJtJCKuqMEa9oYoNx+DwRKt5zjeiHfVxjjDoauFQ8qP2WOuvsqdR0w== +"@sphereon/oid4vci-client@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.10.3.tgz#b8be701f9d2de9daa9e0f81309024a89956d6e09" + integrity sha512-PkIZrwTMrHlgwcDNimWDQaAgi+9ptkV79g/sQJJAe4g8NCt3WyXtsV9l88CdzxDGVGDtzsnYqPXkimxP4eSScw== dependencies: - "@sphereon/oid4vci-common" "0.10.1" - "@sphereon/ssi-types" "^0.18.1" + "@sphereon/oid4vci-common" "0.10.3" + "@sphereon/ssi-types" "^0.23.0" cross-fetch "^3.1.8" debug "^4.3.4" -"@sphereon/oid4vci-common@0.10.1", "@sphereon/oid4vci-common@^0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.10.1.tgz#49bc77bcdef0e9696526e9517a3caed3fc134804" - integrity sha512-J5MdekO5/EgA7UCpMFdPgAnift1vzvauH5ll19iYZoxKlTL1DZ1yiablo47l3aaral7DASM99HJyHfL7ceGcvg== +"@sphereon/oid4vci-common@0.10.3", "@sphereon/oid4vci-common@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.10.3.tgz#a82359e7aae8d40f7833cf1238f018ec57865c52" + integrity sha512-VsUnDKkKm2yQ3lzAt2CY6vL06mZDK9dhwFT6T92aq03ncbUcS6gelwccdsXEMEfi5r4baFemiFM1O5v+mPjuEA== dependencies: - "@sphereon/ssi-types" "^0.18.1" + "@sphereon/ssi-types" "^0.23.0" cross-fetch "^3.1.8" jwt-decode "^3.1.2" sha.js "^2.4.11" uint8arrays "3.1.1" -"@sphereon/oid4vci-issuer@^0.10.2": - version "0.10.2" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.10.2.tgz#9d9d2ac73927b59e9feba784d1ea87971af7281e" - integrity sha512-9EteuVxZe2tWfmISLelDWBhSzN4s/TAg74z9VDHoyzX/4EED/wtCYXny8JZRwBZAAc9Pdl/3qADe15d3rOQqJw== +"@sphereon/oid4vci-issuer@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.10.3.tgz#1df841656e56cc4eff7d73f1452a92e2221a55f6" + integrity sha512-qhm8ypkXuYsaG5XmXIFwL9DUJQ0TJScNjvg5w7beAm+zjz0sOkwIjXdS7S+29LfWj0BkYiRZp1d3mj8H/rmdUw== dependencies: - "@sphereon/oid4vci-common" "0.10.1" - "@sphereon/ssi-types" "^0.18.1" + "@sphereon/oid4vci-common" "0.10.3" + "@sphereon/ssi-types" "^0.23.0" uuid "^9.0.0" "@sphereon/pex-models@^2.2.4": @@ -2642,14 +2621,6 @@ "@sd-jwt/decode" "^0.6.1" jwt-decode "^3.1.2" -"@sphereon/ssi-types@^0.18.1": - version "0.18.1" - resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.18.1.tgz#c00e4939149f4e441fae56af860735886a4c33a5" - integrity sha512-uM0gb1woyc0R+p+qh8tVDi15ZWmpzo9BP0iBp/yRkJar7gAfgwox/yvtEToaH9jROKnDCwL3DDQCDeNucpMkwg== - dependencies: - "@sd-jwt/decode" "^0.2.0" - jwt-decode "^3.1.2" - "@sphereon/ssi-types@^0.23.0": version "0.23.0" resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.23.0.tgz#e2d6a2a0edfa465bb1ae67c5579dd2aa045403e9" @@ -4079,14 +4050,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@*, buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -4095,6 +4058,14 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -10980,16 +10951,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11063,7 +11025,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11077,13 +11039,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -12003,7 +11958,7 @@ wordwrapjs@^4.0.0: reduce-flatten "^2.0.0" typical "^5.2.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -12021,15 +11976,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"