Skip to content

Commit

Permalink
feat: trusted certificates hook for presentations (#2052)
Browse files Browse the repository at this point in the history
Signed-off-by: Tom Lanser <tom@devv.nl>
  • Loading branch information
Tommylans authored Oct 8, 2024
1 parent 8fdf41d commit 08a485b
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 8 deletions.
20 changes: 14 additions & 6 deletions packages/core/src/crypto/JwsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ export class JwsService {
/**
* Verify a JWS
*/
public async verifyJws(agentContext: AgentContext, { jws, jwkResolver }: VerifyJwsOptions): Promise<VerifyJwsResult> {
public async verifyJws(
agentContext: AgentContext,
{ jws, jwkResolver, trustedCertificates }: VerifyJwsOptions
): Promise<VerifyJwsResult> {
let signatures: JwsDetachedFormat[] = []
let payload: string

Expand Down Expand Up @@ -162,6 +165,7 @@ export class JwsService {
alg: protectedJson.alg,
},
jwkResolver,
trustedCertificates,
})
if (!jwk.supportsSignatureAlgorithm(protectedJson.alg)) {
throw new CredoError(
Expand Down Expand Up @@ -223,9 +227,10 @@ export class JwsService {
protectedHeader: { alg: string; [key: string]: unknown }
payload: string
jwkResolver?: JwsJwkResolver
trustedCertificates?: [string, ...string[]]
}
): Promise<Jwk> {
const { protectedHeader, jwkResolver, jws, payload } = 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.')
Expand All @@ -239,16 +244,17 @@ export class JwsService {
throw new CredoError('x5c header is not a valid JSON array of string.')
}

const trustedCertificates = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates
if (!trustedCertificates) {
const trustedCertificatesFromConfig = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates
const trustedCertificates = [...(trustedCertificatesFromConfig ?? []), ...(trustedCertificatesFromOptions ?? [])]
if (trustedCertificates.length === 0) {
throw new CredoError(
'No trusted certificates configured for X509 certificate chain validation. Issuer cannot be verified.'
`trustedCertificates is required when the JWS protected header contains an 'x5c' property.`
)
}

await X509Service.validateCertificateChain(agentContext, {
certificateChain: protectedHeader.x5c,
trustedCertificates,
trustedCertificates: trustedCertificates as [string, ...string[]], // Already validated that it has at least one certificate
})

const certificate = X509Service.getLeafCertificate(agentContext, { certificateChain: protectedHeader.x5c })
Expand Down Expand Up @@ -308,6 +314,8 @@ export interface VerifyJwsOptions {
* base on the `iss` property in the JWT payload.
*/
jwkResolver?: JwsJwkResolver

trustedCertificates?: [string, ...string[]]
}

export type JwsJwkResolver = (options: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export class DifPresentationExchangeProofFormatService

public async processPresentation(
agentContext: AgentContext,
{ requestAttachment, attachment }: ProofFormatProcessPresentationOptions
{ requestAttachment, attachment, proofRecord }: ProofFormatProcessPresentationOptions
): Promise<boolean> {
const ps = this.presentationExchangeService(agentContext)
const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService)
Expand Down Expand Up @@ -298,6 +298,9 @@ export class DifPresentationExchangeProofFormatService
presentation: parsedPresentation,
challenge: request.options.challenge,
domain: request.options.domain,
verificationContext: {
didcommProofRecordId: proofRecord.id,
},
})
} else if (parsedPresentation.claimFormat === ClaimFormat.LdpVp) {
if (
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/modules/vc/W3cCredentialServiceOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,22 @@ 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
}

export interface W3cJsonLdVerifyPresentationOptions extends W3cVerifyPresentationOptionsBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CredoError } from '../../../error'
import { injectable } from '../../../plugins'
import { asArray, isDid, MessageValidator } from '../../../utils'
import { getKeyDidMappingByKeyType, DidResolverService, getKeyFromVerificationMethod } from '../../dids'
import { X509ModuleConfig } from '../../x509'
import { W3cJsonLdVerifiableCredential } from '../data-integrity'

import { W3cJwtVerifiableCredential } from './W3cJwtVerifiableCredential'
Expand Down Expand Up @@ -308,13 +309,20 @@ export class W3cJwtCredentialService {
const proverPublicKey = getKeyFromVerificationMethod(proverVerificationMethod)
const proverPublicJwk = getJwkFromKey(proverPublicKey)

const getTrustedCertificatesForVerification = agentContext.dependencyManager.isRegistered(X509ModuleConfig)
? agentContext.dependencyManager.resolve(X509ModuleConfig).getTrustedCertificatesForVerification
: undefined

let signatureResult: VerifyJwsResult | undefined = undefined
try {
// Verify the JWS signature
signatureResult = await this.jwsService.verifyJws(agentContext, {
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)),
})

if (!signatureResult.isValid) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getJwkFromKey } from '../../../../crypto/jose/jwk'
import { CredoError, ClassValidationError } from '../../../../error'
import { JsonTransformer } from '../../../../utils'
import { DidJwk, DidKey, DidRepository, DidsModuleConfig } from '../../../dids'
import { X509ModuleConfig } from '../../../x509'
import { CREDENTIALS_CONTEXT_V1_URL } from '../../constants'
import { ClaimFormat, W3cCredential, W3cPresentation } from '../../models'
import { W3cJwtCredentialService } from '../W3cJwtCredentialService'
Expand All @@ -30,6 +31,7 @@ const agentContext = getAgentContext({
[InjectionSymbols.Logger, testLogger],
[DidsModuleConfig, new DidsModuleConfig()],
[DidRepository, {} as unknown as DidRepository],
[X509ModuleConfig, new X509ModuleConfig()],
],
agentConfig: config,
})
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/modules/x509/X509ModuleConfig.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,47 @@
import type { AgentContext } from '../../agent'
import type { VerificationContext } from '../vc'

export interface X509ModuleConfigOptions {
/**
*
* Array of trusted base64-encoded certificate strings in the DER-format.
*/
trustedCertificates?: [string, ...string[]]

/**
* 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.
*
* @returns An array of base64-encoded certificate strings or PEM certificate strings.
*/
getTrustedCertificatesForVerification?(
agentContext: AgentContext,
verificationContext?: VerificationContext
): Promise<[string, ...string[]] | undefined>
}

export class X509ModuleConfig {
private options: X509ModuleConfigOptions

public constructor(options?: X509ModuleConfigOptions) {
this.options = options?.trustedCertificates ? { trustedCertificates: [...options.trustedCertificates] } : {}
this.options.getTrustedCertificatesForVerification = options?.getTrustedCertificatesForVerification
}

public get trustedCertificates() {
return this.options.trustedCertificates
}

public get getTrustedCertificatesForVerification() {
return this.options.getTrustedCertificatesForVerification
}

public setTrustedCertificatesForVerification(fn: X509ModuleConfigOptions['getTrustedCertificatesForVerification']) {
this.options.getTrustedCertificatesForVerification = fn
}

public setTrustedCertificates(trustedCertificates?: [string, ...string[]]) {
this.options.trustedCertificates = trustedCertificates ? [...trustedCertificates] : undefined
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export class OpenId4VcSiopVerifierService {
presentationDefinitions: presentationDefinitionsWithLocation,
verification: {
presentationVerificationCallback: this.getPresentationVerificationCallback(agentContext, {
correlationId: options.verificationSession.id,
nonce: requestNonce,
audience: requestClientId,
}),
Expand Down Expand Up @@ -591,7 +592,7 @@ export class OpenId4VcSiopVerifierService {

private getPresentationVerificationCallback(
agentContext: AgentContext,
options: { nonce: string; audience: string }
options: { nonce: string; audience: string; correlationId: string }
): PresentationVerificationCallback {
return async (encodedPresentation, presentationSubmission) => {
try {
Expand Down Expand Up @@ -621,6 +622,9 @@ export class OpenId4VcSiopVerifierService {
presentation: encodedPresentation,
challenge: options.nonce,
domain: options.audience,
verificationContext: {
openId4VcVerificationSessionId: options.correlationId,
},
})

isValid = verificationResult.isValid
Expand Down

0 comments on commit 08a485b

Please sign in to comment.