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 53073483a7..44536a414c 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, getUnqualifiedRevocationRegistryDefinitionId, isIndyDid, isUnqualifiedCredentialDefinitionId, 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..f4a054e536 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -13,18 +13,22 @@ 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 { TypedArrayEncoder, Hasher } from '../../utils' +import { JwtPayload, Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto' +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 @@ -44,7 +48,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 +91,25 @@ 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'") + } + 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: nowInSeconds(), + vct: payload.vct, + }, disclosureFrame as DisclosureFrame, { header } ) @@ -133,9 +148,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 +181,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 CredoError('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, + }) + + const requiredKeys = requiredClaimKeys ? [...requiredClaimKeys, 'vct'] : ['vct'] - // 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') + 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 +341,6 @@ export class SdJwtVcService { } } - private get hasher(): HasherSync { - return Hasher.hash - } - /** * @todo validate the JWT header (alg) */ @@ -448,4 +513,31 @@ export class SdJwtVcService { throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.") } + + private getBaseSdJwtConfig(agentContext: AgentContext): SdJwtVcConfig { + return { + hasher: this.hasher, + statusListFetcher: this.getStatusListFetcher(agentContext), + saltGenerator: agentContext.wallet.generateNonce, + } + } + + 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 CredoError( + `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..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 @@ -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,132 @@ 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) + + // 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 +875,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 +924,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 +947,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 +978,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/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) +} 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 diff --git a/yarn.lock b/yarn.lock index f7c1a1bdf5..f771a7d461 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,7 +2391,33 @@ "@sd-jwt/types" "0.6.1" "@sd-jwt/utils" "0.6.1" -"@sd-jwt/present@0.6.1", "@sd-jwt/present@^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/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== @@ -2400,12 +2426,25 @@ "@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.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" integrity sha512-LKpABZJGT77jNhOLvAHIkNNmGqXzyfwBT+6r+DN9zNzMx1CzuNR0qXk1GMUbast9iCfPkGbnEpUv/jHTBvlIvg== -"@sd-jwt/utils@0.6.1", "@sd-jwt/utils@^0.6.1": +"@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.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== @@ -2413,6 +2452,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" @@ -9561,7 +9608,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==