diff --git a/.changeset/late-shirts-flash.md b/.changeset/late-shirts-flash.md new file mode 100644 index 0000000000..06b0c8b152 --- /dev/null +++ b/.changeset/late-shirts-flash.md @@ -0,0 +1,6 @@ +--- +'@credo-ts/openid4vc': minor +'@credo-ts/core': minor +--- + +feat: allow dynamicaly providing x509 certificates for all types of verifications diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index 7a3aa5a23b..453825ec68 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -9,6 +9,7 @@ import { DidKey, DidJwk, getJwkFromKey, + X509Module, } from '@credo-ts/core' import { authorizationCodeGrantIdentifier, @@ -19,12 +20,26 @@ import { import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { BaseAgent } from './BaseAgent' -import { Output } from './OutputClass' +import { greenText, Output } from './OutputClass' function getOpenIdHolderModules() { return { askar: new AskarModule({ ariesAskar }), openId4VcHolder: new OpenId4VcHolderModule(), + x509: new X509Module({ + getTrustedCertificatesForVerification: (agentContext, { certificateChain, verification }) => { + console.log( + greenText( + `dyncamically trusting certificate ${certificateChain[0].getIssuerNameField('C')} for verification of ${ + verification.type + }`, + true + ) + ) + + return [certificateChain[0].toString('pem')] + }, + }), } as const } @@ -41,9 +56,6 @@ export class Holder extends BaseAgent> public static async build(): Promise { const holder = new Holder(3000, 'OpenId4VcHolder ' + Math.random().toString()) await holder.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598e') - await holder.agent.x509.addTrustedCertificate( - 'MIH7MIGioAMCAQICEFvUcSkwWUaPlEWnrOmu_EYwCgYIKoZIzj0EAwIwDTELMAkGA1UEBhMCREUwIBcNMDAwMTAxMDAwMDAwWhgPMjA1MDAxMDEwMDAwMDBaMA0xCzAJBgNVBAYTAkRFMDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgAC3A9V8ynqRcVjADqlfpZ9X8mwbew0TuQldH_QOpkadsWjAjAAMAoGCCqGSM49BAMCA0gAMEUCIQDXGNookSkHqRXiOP_0fVUdNIScY13h3DWkqSopFIYB2QIgUzNFnZ-SEdm-7UMzggaPiFgtznVzmHw2h4vVtuLzWlA' - ) return holder } diff --git a/packages/core/src/crypto/JwsService.ts b/packages/core/src/crypto/JwsService.ts index eed23e2df2..31f04163a5 100644 --- a/packages/core/src/crypto/JwsService.ts +++ b/packages/core/src/crypto/JwsService.ts @@ -12,7 +12,7 @@ import type { AgentContext } from '../agent' import type { Buffer } from '../utils' import { CredoError } from '../error' -import { X509ModuleConfig } from '../modules/x509' +import { EncodedX509Certificate, X509ModuleConfig } from '../modules/x509' import { injectable } from '../plugins' import { isJsonObject, JsonEncoder, TypedArrayEncoder } from '../utils' import { WalletError } from '../wallet/error' @@ -227,10 +227,16 @@ export class JwsService { protectedHeader: { alg: string; [key: string]: unknown } payload: string jwkResolver?: JwsJwkResolver - trustedCertificates?: [string, ...string[]] + trustedCertificates?: EncodedX509Certificate[] } ): Promise { - const { protectedHeader, jwkResolver, jws, payload, trustedCertificates: trustedCertificatesFromOptions } = options + const { + protectedHeader, + jwkResolver, + jws, + payload, + trustedCertificates: trustedCertificatesFromOptions = [], + } = options if ([protectedHeader.jwk, protectedHeader.kid, protectedHeader.x5c].filter(Boolean).length > 1) { throw new CredoError('Only one of jwk, kid and x5c headers can and must be provided.') @@ -244,8 +250,9 @@ export class JwsService { throw new CredoError('x5c header is not a valid JSON array of string.') } - const trustedCertificatesFromConfig = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates - const trustedCertificates = [...(trustedCertificatesFromConfig ?? []), ...(trustedCertificatesFromOptions ?? [])] + const trustedCertificatesFromConfig = + agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates ?? [] + const trustedCertificates = trustedCertificatesFromOptions ?? trustedCertificatesFromConfig if (trustedCertificates.length === 0) { throw new CredoError( `trustedCertificates is required when the JWS protected header contains an 'x5c' property.` @@ -254,7 +261,7 @@ export class JwsService { await X509Service.validateCertificateChain(agentContext, { certificateChain: protectedHeader.x5c, - trustedCertificates: trustedCertificates as [string, ...string[]], // Already validated that it has at least one certificate + trustedCertificates, }) const certificate = X509Service.getLeafCertificate(agentContext, { certificateChain: protectedHeader.x5c }) @@ -315,7 +322,7 @@ export interface VerifyJwsOptions { */ jwkResolver?: JwsJwkResolver - trustedCertificates?: [string, ...string[]] + trustedCertificates?: EncodedX509Certificate[] } export type JwsJwkResolver = (options: { diff --git a/packages/core/src/crypto/jose/jwt/Jwt.ts b/packages/core/src/crypto/jose/jwt/Jwt.ts index 6de8095c41..b55b77b0df 100644 --- a/packages/core/src/crypto/jose/jwt/Jwt.ts +++ b/packages/core/src/crypto/jose/jwt/Jwt.ts @@ -11,6 +11,7 @@ interface JwtHeader { alg: string kid?: string jwk?: JwkJson + x5c?: string[] [key: string]: unknown } diff --git a/packages/core/src/modules/mdoc/Mdoc.ts b/packages/core/src/modules/mdoc/Mdoc.ts index 1cf26c328d..7362c03a11 100644 --- a/packages/core/src/modules/mdoc/Mdoc.ts +++ b/packages/core/src/modules/mdoc/Mdoc.ts @@ -88,6 +88,10 @@ export class Mdoc { ) } + public get issuerSignedCertificateChain() { + return this.issuerSignedDocument.issuerSigned.issuerAuth.certificateChain + } + public get issuerSignedNamespaces(): MdocNameSpaces { return Object.fromEntries( Array.from(this.issuerSignedDocument.allIssuerSignedNamespaces.entries()).map(([namespace, value]) => [ @@ -156,16 +160,22 @@ export class Mdoc { agentContext: AgentContext, options?: MdocVerifyOptions ): Promise<{ isValid: true } | { isValid: false; error: string }> { - let trustedCerts: [string, ...string[]] | undefined - - if (options?.trustedCertificates) { - trustedCerts = options.trustedCertificates - } else if (options?.verificationContext) { - trustedCerts = await agentContext.dependencyManager - .resolve(X509ModuleConfig) - .getTrustedCertificatesForVerification?.(agentContext, options.verificationContext) - } else { - trustedCerts = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates + const x509ModuleConfig = agentContext.dependencyManager.resolve(X509ModuleConfig) + const certificateChain = this.issuerSignedDocument.issuerSigned.issuerAuth.certificateChain.map( + X509Certificate.fromRawCertificate + ) + + let trustedCerts = options?.trustedCertificates + if (!trustedCerts) { + // TODO: how to prevent call to trusted certificates for verification twice? + trustedCerts = + (await x509ModuleConfig.getTrustedCertificatesForVerification?.(agentContext, { + verification: { + type: 'credential', + credential: this, + }, + certificateChain, + })) ?? x509ModuleConfig.trustedCertificates } if (!trustedCerts) { diff --git a/packages/core/src/modules/mdoc/MdocDeviceResponse.ts b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts index dda7266c86..03af7dfeea 100644 --- a/packages/core/src/modules/mdoc/MdocDeviceResponse.ts +++ b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts @@ -47,6 +47,7 @@ export class MdocDeviceResponse { docType ) }) + documents[0].deviceSignedNamespaces return new MdocDeviceResponse(base64Url, documents) } @@ -197,14 +198,30 @@ export class MdocDeviceResponse { public async verify(agentContext: AgentContext, options: Omit) { const verifier = new Verifier() const mdocContext = getMdocContext(agentContext) + const x509Config = agentContext.dependencyManager.resolve(X509ModuleConfig) - const x509ModuleConfig = agentContext.dependencyManager.resolve(X509ModuleConfig) - const getTrustedCertificatesForVerification = x509ModuleConfig.getTrustedCertificatesForVerification - - const trustedCertificates = - options.trustedCertificates ?? - (await getTrustedCertificatesForVerification?.(agentContext, options.verificationContext)) ?? - x509ModuleConfig?.trustedCertificates + // TODO: no way to currently have a per document x509 certificates in a presentation + // but this also the case for other + // FIXME: we can't pass multiple certificate chains. We should just verify each document separately + let trustedCertificates = options.trustedCertificates + if (!trustedCertificates) { + trustedCertificates = ( + await Promise.all( + this.documents.map((mdoc) => { + const certificateChain = mdoc.issuerSignedCertificateChain.map(X509Certificate.fromRawCertificate) + return x509Config.getTrustedCertificatesForVerification?.(agentContext, { + certificateChain, + verification: { + type: 'credential', + credential: mdoc, + }, + }) + }) + ) + ) + .filter((c): c is string[] => c !== undefined) + .flatMap((c) => c) + } if (!trustedCertificates) { throw new MdocError('No trusted certificates found. Cannot verify mdoc.') diff --git a/packages/core/src/modules/mdoc/MdocOptions.ts b/packages/core/src/modules/mdoc/MdocOptions.ts index 843b923915..a765c6760a 100644 --- a/packages/core/src/modules/mdoc/MdocOptions.ts +++ b/packages/core/src/modules/mdoc/MdocOptions.ts @@ -1,21 +1,14 @@ import type { Mdoc } from './Mdoc' import type { Key } from '../../crypto/Key' import type { DifPresentationExchangeDefinition } from '../dif-presentation-exchange' +import type { EncodedX509Certificate } from '../x509' import type { ValidityInfo } from '@animo-id/mdoc' export type MdocNameSpaces = Record> -export interface MdocVerificationContext { - /** - * The `id` of the `OpenId4VcVerificationSessionRecord` that this verification is bound to. - */ - openId4VcVerificationSessionId?: string -} - export type MdocVerifyOptions = { - trustedCertificates?: [string, ...string[]] + trustedCertificates?: EncodedX509Certificate[] now?: Date - verificationContext?: MdocVerificationContext } export type MdocOpenId4VpSessionTranscriptOptions = { @@ -33,14 +26,13 @@ export type MdocDeviceResponseOpenId4VpOptions = { } export type MdocDeviceResponseVerifyOptions = { - trustedCertificates?: [string, ...string[]] + trustedCertificates?: EncodedX509Certificate[] sessionTranscriptOptions: MdocOpenId4VpSessionTranscriptOptions /** * The base64Url-encoded device response string. */ deviceResponse: string now?: Date - verificationContext?: MdocVerificationContext } export type MdocSignOptions = { diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 0a35d13af2..4b4cd7b806 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -48,6 +48,7 @@ import { W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation, } from '../../../vc' +import { extractX509CertificatesFromJwt, X509ModuleConfig } from '../../../x509' import { ProofFormatSpec } from '../../models' const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' @@ -301,13 +302,25 @@ export class DifPresentationExchangeProofFormatService // whether it's a JWT or JSON-LD VP even though the input is the same. // Not sure how to fix if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { + const x509Config = agentContext.dependencyManager.resolve(X509ModuleConfig) + + const certificateChain = extractX509CertificatesFromJwt(parsedPresentation.jwt) + const trustedCertificates = certificateChain + ? await x509Config.getTrustedCertificatesForVerification?.(agentContext, { + certificateChain, + verification: { + type: 'credential', + credential: parsedPresentation, + didcommProofRecordId: proofRecord.id, + }, + }) + : x509Config.trustedCertificates ?? [] + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { presentation: parsedPresentation, challenge: request.options.challenge, domain: request.options.domain, - verificationContext: { - didcommProofRecordId: proofRecord.id, - }, + trustedCertificates, }) } else if (parsedPresentation.claimFormat === ClaimFormat.LdpVp) { if ( diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts index 1167ddc973..cace5b596c 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts @@ -1,4 +1,5 @@ import type { JwkJson, Jwk, HashName } from '../../crypto' +import type { EncodedX509Certificate } from '../x509' // TODO: extend with required claim names for input (e.g. vct) export type SdJwtVcPayload = Record @@ -125,4 +126,6 @@ export type SdJwtVcVerifyOptions = { * It will will not influence the verification result if fetching of type metadata fails */ fetchTypeMetadata?: boolean + + trustedCertificates?: EncodedX509Certificate[] } diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts index 346e512737..00c8d47d2b 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -24,7 +24,7 @@ import { TypedArrayEncoder, nowInSeconds } from '../../utils' import { getDomainFromUrl } from '../../utils/domain' import { fetchWithTimeout } from '../../utils/fetch' import { DidResolverService, parseDid, getKeyFromVerificationMethod } from '../dids' -import { X509Certificate, X509ModuleConfig } from '../x509' +import { EncodedX509Certificate, X509Certificate, X509ModuleConfig } from '../x509' import { SdJwtVcError } from './SdJwtVcError' import { decodeSdJwtVc, sdJwtVcHasher } from './decodeSdJwtVc' @@ -191,7 +191,7 @@ export class SdJwtVcService { public async verify
( agentContext: AgentContext, - { compactSdJwtVc, keyBinding, requiredClaimKeys, fetchTypeMetadata }: SdJwtVcVerifyOptions + { compactSdJwtVc, keyBinding, requiredClaimKeys, fetchTypeMetadata, trustedCertificates }: SdJwtVcVerifyOptions ): Promise< | { isValid: true; verification: VerificationResult; sdJwtVc: SdJwtVc } | { isValid: false; verification: VerificationResult; sdJwtVc?: SdJwtVc; error: Error } @@ -229,7 +229,12 @@ export class SdJwtVcService { } satisfies SdJwtVc try { - const credentialIssuer = await this.parseIssuerFromCredential(agentContext, sdJwtVc) + const credentialIssuer = await this.parseIssuerFromCredential( + agentContext, + sdJwtVc, + returnSdJwtVc, + trustedCertificates + ) const issuer = await this.extractKeyFromIssuer(agentContext, credentialIssuer) const holderBinding = this.parseHolderBindingFromCredential(sdJwtVc) const holder = holderBinding ? await this.extractKeyFromHolderBinding(agentContext, holderBinding) : undefined @@ -455,8 +460,11 @@ export class SdJwtVcService { private async parseIssuerFromCredential
( agentContext: AgentContext, - sdJwtVc: SDJwt + sdJwtVc: SDJwt, + credoSdJwtVc: SdJwtVc, + _trustedCertificates?: EncodedX509Certificate[] ): Promise { + const x509Config = agentContext.dependencyManager.resolve(X509ModuleConfig) if (!sdJwtVc.jwt?.payload) { throw new SdJwtVcError('Credential not exist') } @@ -478,7 +486,18 @@ export class SdJwtVcService { throw new SdJwtVcError('Invalid x5c header in credential. Not an array of strings.') } - const trustedCertificates = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates + let trustedCertificates = _trustedCertificates + const certificateChain = sdJwtVc.jwt.header.x5c.map(X509Certificate.fromEncodedCertificate) + if (certificateChain && !trustedCertificates) { + trustedCertificates = + (await x509Config.getTrustedCertificatesForVerification?.(agentContext, { + certificateChain, + verification: { + type: 'credential', + credential: credoSdJwtVc, + }, + })) ?? x509Config.trustedCertificates + } if (!trustedCertificates) { throw new SdJwtVcError( 'No trusted certificates configured for X509 certificate chain validation. Issuer cannot be verified.' diff --git a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts index 10e7679016..503d58818b 100644 --- a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts +++ b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts @@ -7,6 +7,7 @@ import type { W3cCredential } from './models/credential/W3cCredential' import type { W3cPresentation } from './models/presentation/W3cPresentation' import type { JwaSignatureAlgorithm } from '../../crypto/jose/jwa' import type { SingleOrArray } from '../../utils/type' +import type { EncodedX509Certificate } from '../x509' export type W3cSignCredentialOptions = Format extends ClaimFormat.JwtVc @@ -179,22 +180,9 @@ interface W3cVerifyPresentationOptionsBase { verifyCredentialStatus?: boolean } -export interface VerificationContext { - /** - * The `id` of the `ProofRecord` that this verification is bound to. - */ - didcommProofRecordId?: string - - /** - * The `id` of the `OpenId4VcVerificationSessionRecord` that this verification is bound to. - */ - openId4VcVerificationSessionId?: string -} - export interface W3cJwtVerifyPresentationOptions extends W3cVerifyPresentationOptionsBase { presentation: W3cJwtVerifiablePresentation | string // string must be encoded VP JWT - trustedCertificates?: [string, ...string[]] - verificationContext?: VerificationContext + trustedCertificates?: EncodedX509Certificate[] } export interface W3cJsonLdVerifyPresentationOptions extends W3cVerifyPresentationOptionsBase { diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts index a03a07528d..106cc66b3b 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts @@ -16,6 +16,7 @@ import { injectable } from '../../../plugins' import { asArray, isDid, MessageValidator } from '../../../utils' import { getKeyDidMappingByKeyType, DidResolverService, getKeyFromVerificationMethod } from '../../dids' import { X509ModuleConfig } from '../../x509' +import { extractX509CertificatesFromJwt } from '../../x509/extraction' import { W3cJsonLdVerifiableCredential } from '../data-integrity' import { W3cJwtVerifiableCredential } from './W3cJwtVerifiableCredential' @@ -309,9 +310,19 @@ export class W3cJwtCredentialService { const proverPublicKey = getKeyFromVerificationMethod(proverVerificationMethod) const proverPublicJwk = getJwkFromKey(proverPublicKey) - const getTrustedCertificatesForVerification = agentContext.dependencyManager.isRegistered(X509ModuleConfig) - ? agentContext.dependencyManager.resolve(X509ModuleConfig).getTrustedCertificatesForVerification - : undefined + let trustedCertificates = options.trustedCertificates + const certificateChain = extractX509CertificatesFromJwt(presentation.jwt) + if (certificateChain && !trustedCertificates) { + const getTrustedCertificatesForVerification = + agentContext.dependencyManager.resolve(X509ModuleConfig).getTrustedCertificatesForVerification + trustedCertificates = await getTrustedCertificatesForVerification?.(agentContext, { + certificateChain, + verification: { + type: 'credential', + credential: presentation, + }, + }) + } let signatureResult: VerifyJwsResult | undefined = undefined try { @@ -320,9 +331,7 @@ export class W3cJwtCredentialService { jws: presentation.jwt.serializedJwt, // We have pre-fetched the key based on the singer/holder of the presentation jwkResolver: () => proverPublicJwk, - trustedCertificates: - options.trustedCertificates ?? - (await getTrustedCertificatesForVerification?.(agentContext, options.verificationContext)), + trustedCertificates, }) if (!signatureResult.isValid) { diff --git a/packages/core/src/modules/x509/X509ModuleConfig.ts b/packages/core/src/modules/x509/X509ModuleConfig.ts index 97ea419393..a6b46e8d91 100644 --- a/packages/core/src/modules/x509/X509ModuleConfig.ts +++ b/packages/core/src/modules/x509/X509ModuleConfig.ts @@ -1,5 +1,46 @@ +import type { X509Certificate } from './X509Certificate' import type { AgentContext } from '../../agent' -import type { VerificationContext } from '../vc' +import type { JwtPayload } from '../../crypto' +import type { Mdoc } from '../mdoc/Mdoc' +import type { MdocDeviceResponse } from '../mdoc/MdocDeviceResponse' +import type { SdJwtVc } from '../sd-jwt-vc' +import type { W3cJwtVerifiableCredential, W3cJwtVerifiablePresentation } from '../vc' + +type X509VerificationTypeCredential = { + type: 'credential' + credential: SdJwtVc | Mdoc | MdocDeviceResponse | W3cJwtVerifiableCredential | W3cJwtVerifiablePresentation + + /** + * The `id` of the `DidCommProofRecord` that this verification is bound to. + */ + didcommProofRecordId?: string + + /** + * The `id` of the `OpenId4VcVerificationSessionRecord` that this verification is bound to. + */ + openId4VcVerificationSessionId?: string +} + +type X509VerificationTypeOauth2SecuredAuthorizationRequest = { + type: 'oauth2SecuredAuthorizationRequest' + authorizationRequest: { + jwt: string + payload: JwtPayload + } +} + +export interface X509VerificationContext { + /** + * The certificate chain provided with the data to be verified. The trusted certificates + * are determined before verification and thus it is not verified that the data was actually + * signed by the private key assocaited with the leaf certificate in the certificate chain, or + * whether the certificate chain is valid. However if the certificate + * does not match, or is not valid, verification will always fail at a later stage + */ + certificateChain: X509Certificate[] + + verification: X509VerificationTypeCredential | X509VerificationTypeOauth2SecuredAuthorizationRequest +} export interface X509ModuleConfigOptions { /** @@ -10,24 +51,28 @@ export interface X509ModuleConfigOptions { /** * Optional callback method that will be called to dynamically get trusted certificates for a verification. - * It will always provide the `agentContext` allowing to dynamically set the trusted certificates for a tenant. - * If available the associated record id is also provided allowing to filter down trusted certificates to a single - * exchange. + * It will provide the `agentContext` and `verificationContext` allowing to dynamically set the trusted certificates + * for a tenant or verificaiton context. + * + * If no certificaets should be trusted an empty array should be returned. If `undefined` is returned + * it will fallback to the globally registered trusted certificates * * @returns An array of base64-encoded certificate strings or PEM certificate strings. */ getTrustedCertificatesForVerification?( agentContext: AgentContext, - verificationContext?: VerificationContext - ): Promise<[string, ...string[]] | undefined> + verificationContext: X509VerificationContext + ): Promise | string[] | undefined } export class X509ModuleConfig { private options: X509ModuleConfigOptions public constructor(options?: X509ModuleConfigOptions) { - this.options = options?.trustedCertificates ? { trustedCertificates: [...options.trustedCertificates] } : {} - this.options.getTrustedCertificatesForVerification = options?.getTrustedCertificatesForVerification + this.options = { + getTrustedCertificatesForVerification: options?.getTrustedCertificatesForVerification, + trustedCertificates: options?.trustedCertificates ? [...options.trustedCertificates] : undefined, + } } public get trustedCertificates() { diff --git a/packages/core/src/modules/x509/X509ServiceOptions.ts b/packages/core/src/modules/x509/X509ServiceOptions.ts index a653c1a0b1..84068af570 100644 --- a/packages/core/src/modules/x509/X509ServiceOptions.ts +++ b/packages/core/src/modules/x509/X509ServiceOptions.ts @@ -1,8 +1,14 @@ import type { ExtensionInput } from './X509Certificate' import type { Key } from '../../crypto/Key' +/** + * Base64 or PEM + */ +export type EncodedX509Certificate = string + export interface X509ValidateCertificateChainOptions { - certificateChain: Array + certificateChain: Array + certificate?: string /** * The date for which the certificate chain should be valid @@ -13,7 +19,8 @@ export interface X509ValidateCertificateChainOptions { * otherwise, the validation will fail */ verificationDate?: Date - trustedCertificates?: [string, ...string[]] + + trustedCertificates?: EncodedX509Certificate[] } export interface X509CreateSelfSignedCertificateOptions { diff --git a/packages/core/src/modules/x509/extraction.ts b/packages/core/src/modules/x509/extraction.ts new file mode 100644 index 0000000000..3dcbd361ad --- /dev/null +++ b/packages/core/src/modules/x509/extraction.ts @@ -0,0 +1,7 @@ +import type { Jwt } from '../../crypto' + +import { X509Certificate } from './X509Certificate' + +export function extractX509CertificatesFromJwt(jwt: Jwt) { + return jwt.header.x5c?.map(X509Certificate.fromEncodedCertificate) +} diff --git a/packages/core/src/modules/x509/index.ts b/packages/core/src/modules/x509/index.ts index 9291eac3d3..abe5a67c0d 100644 --- a/packages/core/src/modules/x509/index.ts +++ b/packages/core/src/modules/x509/index.ts @@ -5,3 +5,4 @@ export * from './X509Api' export * from './X509Module' export * from './X509ModuleConfig' export * from './X509ServiceOptions' +export * from './extraction' diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index 122916e4b3..33f8071052 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -3,7 +3,7 @@ import type { OpenId4VcSiopResolvedAuthorizationRequest, } from './OpenId4vcSiopHolderServiceOptions' import type { OpenId4VcJwtIssuer } from '../shared' -import type { AgentContext, JwkJson, VerifiablePresentation } from '@credo-ts/core' +import type { AgentContext, EncodedX509Certificate, JwkJson, VerifiablePresentation } from '@credo-ts/core' import type { AuthorizationResponsePayload, PresentationExchangeResponseOpts, @@ -38,9 +38,10 @@ export class OpenId4VcSiopHolderService { public async resolveAuthorizationRequest( agentContext: AgentContext, - requestJwtOrUri: string + requestJwtOrUri: string, + trustedCertificates?: EncodedX509Certificate[] ): Promise { - const openidProvider = await this.getOpenIdProvider(agentContext) + const openidProvider = await this.getOpenIdProvider(agentContext, trustedCertificates) // parsing happens automatically in verifyAuthorizationRequest const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri) @@ -244,7 +245,7 @@ export class OpenId4VcSiopHolderService { } as const } - private async getOpenIdProvider(agentContext: AgentContext) { + private async getOpenIdProvider(agentContext: AgentContext, trustedCertificates?: EncodedX509Certificate[]) { const builder = OP.builder() .withExpiresIn(6000) .withIssuer(ResponseIss.SELF_ISSUED_V2) @@ -255,7 +256,7 @@ export class OpenId4VcSiopHolderService { SupportedVersion.SIOPv2_D12_OID4VP_D20, ]) .withCreateJwtCallback(getCreateJwtCallback(agentContext)) - .withVerifyJwtCallback(getVerifyJwtCallback(agentContext)) + .withVerifyJwtCallback(getVerifyJwtCallback(agentContext, trustedCertificates)) .withHasher(Hasher.hash) const openidProvider = builder.build() diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 6d8196056b..a26e5a5a18 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -44,6 +44,10 @@ import { TypedArrayEncoder, Jwt, extractPresentationsWithDescriptorsFromSubmission, + X509ModuleConfig, + extractX509CertificatesFromJwt, + W3cJwtVerifiablePresentation, + X509Certificate, } from '@credo-ts/core' import { AuthorizationRequest, @@ -296,6 +300,7 @@ export class OpenId4VcSiopVerifierService { mdocGeneratedNonce: options.jarmHeader?.apu ? TypedArrayEncoder.toUtf8String(TypedArrayEncoder.fromBase64(options.jarmHeader.apu)) : undefined, + verificationSessionRecordId: options.verificationSession.id, }), }, }) @@ -627,6 +632,7 @@ export class OpenId4VcSiopVerifierService { correlationId: string responseUri?: string mdocGeneratedNonce?: string + verificationSessionRecordId: string } ): PresentationVerificationCallback { return async (encodedPresentation, presentationSubmission) => { @@ -635,6 +641,7 @@ export class OpenId4VcSiopVerifierService { this.logger.debug(`Presentation submission`, presentationSubmission) if (!encodedPresentation) throw new CredoError('Did not receive a presentation for verification.') + const x509Config = agentContext.dependencyManager.resolve(X509ModuleConfig) let isValid: boolean let reason: string | undefined = undefined @@ -642,15 +649,31 @@ export class OpenId4VcSiopVerifierService { if (typeof encodedPresentation === 'string' && encodedPresentation.includes('~')) { // TODO: it might be better here to look at the presentation submission to know // If presentation includes a ~, we assume it's an SD-JWT-VC - const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) + const jwt = Jwt.fromSerializedJwt(encodedPresentation.split('~')[0]) + const sdJwtVc = sdJwtVcApi.fromCompact(encodedPresentation) + const certificateChain = extractX509CertificatesFromJwt(jwt) + const trustedCertificates = certificateChain + ? await x509Config.getTrustedCertificatesForVerification?.(agentContext, { + certificateChain, + verification: { + type: 'credential', + credential: sdJwtVc, + + openId4VcVerificationSessionId: options.verificationSessionRecordId, + }, + }) + : // We also take from the config here to avoid the callback being called again + x509Config.trustedCertificates ?? [] + const verificationResult = await sdJwtVcApi.verify({ compactSdJwtVc: encodedPresentation, keyBinding: { audience: options.audience, nonce: options.nonce, }, + trustedCertificates, }) isValid = verificationResult.verification.isValid @@ -661,6 +684,28 @@ export class OpenId4VcSiopVerifierService { reason = 'Mdoc device response verification failed. Response uri and the mdocGeneratedNonce are not set' } else { const mdocDeviceResponse = MdocDeviceResponse.fromBase64Url(encodedPresentation) + + const trustedCertificates = ( + await Promise.all( + mdocDeviceResponse.documents.map(async (mdoc) => { + const certificateChain = mdoc.issuerSignedCertificateChain.map(X509Certificate.fromRawCertificate) + return ( + (await x509Config.getTrustedCertificatesForVerification?.(agentContext, { + certificateChain, + verification: { + type: 'credential', + credential: mdoc, + openId4VcVerificationSessionId: options.verificationSessionRecordId, + }, + // TODO: could have some duplication but not a big issue + })) ?? x509Config.trustedCertificates + ) + }) + ) + ) + .filter((c): c is string[] => c !== undefined) + .flatMap((c) => c) + await mdocDeviceResponse.verify(agentContext, { sessionTranscriptOptions: { clientId: options.audience, @@ -668,20 +713,30 @@ export class OpenId4VcSiopVerifierService { responseUri: options.responseUri, verifierGeneratedNonce: options.nonce, }, - verificationContext: { - openId4VcVerificationSessionId: options.correlationId, - }, + trustedCertificates, }) isValid = true } } else if (typeof encodedPresentation === 'string' && Jwt.format.test(encodedPresentation)) { + const presentation = W3cJwtVerifiablePresentation.fromSerializedJwt(encodedPresentation) + const certificateChain = extractX509CertificatesFromJwt(presentation.jwt) + const trustedCertificates = certificateChain + ? await x509Config.getTrustedCertificatesForVerification?.(agentContext, { + certificateChain, + verification: { + type: 'credential', + credential: presentation, + + openId4VcVerificationSessionId: options.verificationSessionRecordId, + }, + }) + : x509Config.trustedCertificates ?? [] + const verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { presentation: encodedPresentation, challenge: options.nonce, domain: options.audience, - verificationContext: { - openId4VcVerificationSessionId: options.correlationId, - }, + trustedCertificates, }) isValid = verificationResult.isValid diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index 9b15bd86ab..84dd2add34 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -1,5 +1,12 @@ import type { OpenId4VcIssuerX5c, OpenId4VcJwtIssuer } from './models' -import type { AgentContext, DidPurpose, JwaSignatureAlgorithm, JwkJson, Key } from '@credo-ts/core' +import type { + AgentContext, + DidPurpose, + EncodedX509Certificate, + JwaSignatureAlgorithm, + JwkJson, + Key, +} from '@credo-ts/core' import type { JwtIssuerWithContext as VpJwtIssuerWithContext, VerifyJwtCallback } from '@sphereon/did-auth-siop' import type { DPoPJwtIssuerWithContext, CreateJwtCallback, JwtIssuer } from '@sphereon/oid4vc-common' @@ -9,6 +16,8 @@ import { JwsService, JwtPayload, SignatureSuiteRegistry, + X509Certificate, + X509ModuleConfig, X509Service, getDomainFromUrl, getJwkClassFromKeyType, @@ -52,17 +61,53 @@ export async function getKeyFromDid( return getKeyFromVerificationMethod(verificationMethod) } -export function getVerifyJwtCallback(agentContext: AgentContext): VerifyJwtCallback { +export function getVerifyJwtCallback( + agentContext: AgentContext, + _trustedCertificates?: EncodedX509Certificate[] +): VerifyJwtCallback { return async (jwtVerifier, jwt) => { const jwsService = agentContext.dependencyManager.resolve(JwsService) + + let trustedCertificates = _trustedCertificates + if (jwtVerifier.method === 'did') { const key = await getKeyFromDid(agentContext, jwtVerifier.didUrl) const jwk = getJwkFromKey(key) - const res = await jwsService.verifyJws(agentContext, { jws: jwt.raw, jwkResolver: () => jwk }) + const res = await jwsService.verifyJws(agentContext, { + jws: jwt.raw, + jwkResolver: () => jwk, + // No certificates trusted + trustedCertificates: [], + }) return res.isValid } else if (jwtVerifier.method === 'x5c' || jwtVerifier.method === 'jwk') { - const res = await jwsService.verifyJws(agentContext, { jws: jwt.raw }) + if (jwtVerifier.type === 'request-object') { + const x509Config = agentContext.dependencyManager.resolve(X509ModuleConfig) + const certificateChain = jwt.header.x5c?.map(X509Certificate.fromEncodedCertificate) + + if (!trustedCertificates) { + trustedCertificates = certificateChain + ? await x509Config.getTrustedCertificatesForVerification?.(agentContext, { + certificateChain, + verification: { + type: 'oauth2SecuredAuthorizationRequest', + authorizationRequest: { + jwt: jwt.raw, + payload: JwtPayload.fromJson(jwt.payload), + }, + }, + }) + : // We also take from the config here to avoid the callback being called again + x509Config.trustedCertificates ?? [] + } + } + + const res = await jwsService.verifyJws(agentContext, { + jws: jwt.raw, + // Only allowed for request object + trustedCertificates: jwtVerifier.type === 'request-object' ? trustedCertificates : [], + }) return res.isValid } else { throw new Error(`Unsupported jwt verifier method: '${jwtVerifier.method}'`)