Skip to content

Commit

Permalink
feat: allow dynamicaly providing x509 certificates for all types of v…
Browse files Browse the repository at this point in the history
…erifications (#2112)

Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra authored Jan 8, 2025
1 parent b41a4b1 commit 5f08bc6
Show file tree
Hide file tree
Showing 24 changed files with 417 additions and 118 deletions.
6 changes: 6 additions & 0 deletions .changeset/late-shirts-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@credo-ts/openid4vc': minor
'@credo-ts/core': minor
---

feat: allow dynamicaly providing x509 certificates for all types of verifications
20 changes: 16 additions & 4 deletions demo-openid/src/Holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DidKey,
DidJwk,
getJwkFromKey,
X509Module,
} from '@credo-ts/core'
import {
authorizationCodeGrantIdentifier,
Expand All @@ -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
}

Expand All @@ -41,9 +56,6 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
public static async build(): Promise<Holder> {
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
}
Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import base from './jest.config.base'
const config: Config.InitialOptions = {
...base,
roots: ['<rootDir>'],
coverageReporters: ['text-summary', 'lcov', 'json'],
coveragePathIgnorePatterns: ['/build/', '/node_modules/', '/__tests__/', 'tests'],
coverageDirectory: '<rootDir>/coverage/',
projects: [
Expand Down
21 changes: 14 additions & 7 deletions packages/core/src/crypto/JwsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -227,10 +227,16 @@ export class JwsService {
protectedHeader: { alg: string; [key: string]: unknown }
payload: string
jwkResolver?: JwsJwkResolver
trustedCertificates?: [string, ...string[]]
trustedCertificates?: EncodedX509Certificate[]
}
): Promise<Jwk> {
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.')
Expand All @@ -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.`
Expand All @@ -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 })
Expand Down Expand Up @@ -315,7 +322,7 @@ export interface VerifyJwsOptions {
*/
jwkResolver?: JwsJwkResolver

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

export type JwsJwkResolver = (options: {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/crypto/jose/jwt/Jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface JwtHeader {
alg: string
kid?: string
jwk?: JwkJson
x5c?: string[]
[key: string]: unknown
}

Expand Down
35 changes: 23 additions & 12 deletions packages/core/src/modules/mdoc/Mdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]) => [
Expand Down Expand Up @@ -156,19 +160,24 @@ 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((certificate) =>
X509Certificate.fromRawCertificate(certificate)
)

let trustedCertificates = options?.trustedCertificates
if (!trustedCertificates) {
trustedCertificates =
(await x509ModuleConfig.getTrustedCertificatesForVerification?.(agentContext, {
verification: {
type: 'credential',
credential: this,
},
certificateChain,
})) ?? x509ModuleConfig.trustedCertificates
}

if (!trustedCerts) {
if (!trustedCertificates) {
throw new MdocError('No trusted certificates found. Cannot verify mdoc.')
}

Expand All @@ -177,7 +186,9 @@ export class Mdoc {
const verifier = new Verifier()
await verifier.verifyIssuerSignature(
{
trustedCertificates: trustedCerts.map((cert) => X509Certificate.fromEncodedCertificate(cert).rawCertificate),
trustedCertificates: trustedCertificates.map(
(cert) => X509Certificate.fromEncodedCertificate(cert).rawCertificate
),
issuerAuth: this.issuerSignedDocument.issuerSigned.issuerAuth,
disableCertificateChainValidation: false,
now: options?.now,
Expand Down
35 changes: 28 additions & 7 deletions packages/core/src/modules/mdoc/MdocDeviceResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class MdocDeviceResponse {
docType
)
})
documents[0].deviceSignedNamespaces

return new MdocDeviceResponse(base64Url, documents)
}
Expand Down Expand Up @@ -197,14 +198,34 @@ export class MdocDeviceResponse {
public async verify(agentContext: AgentContext, options: Omit<MdocDeviceResponseVerifyOptions, 'deviceResponse'>) {
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 formats
// 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((cert) =>
X509Certificate.fromRawCertificate(cert)
)
return (
x509Config.getTrustedCertificatesForVerification?.(agentContext, {
certificateChain,
verification: {
type: 'credential',
credential: mdoc,
},
}) ?? x509Config.trustedCertificates
)
})
)
)
.filter((c): c is string[] => c !== undefined)
.flatMap((c) => c)
}

if (!trustedCertificates) {
throw new MdocError('No trusted certificates found. Cannot verify mdoc.')
Expand Down
14 changes: 3 additions & 11 deletions packages/core/src/modules/mdoc/MdocOptions.ts
Original file line number Diff line number Diff line change
@@ -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<string, Record<string, unknown>>

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 = {
Expand All @@ -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 = {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AgentContext } from '../../..'

import { KeyType, X509Service } from '../../..'
import { KeyType, X509ModuleConfig, X509Service } from '../../..'
import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet'
import { getAgentConfig, getAgentContext } from '../../../../tests'
import { Mdoc } from '../Mdoc'
Expand All @@ -14,7 +14,7 @@ describe('mdoc service test', () => {
beforeAll(async () => {
const agentConfig = getAgentConfig('mdoc')
wallet = new InMemoryWallet()
agentContext = getAgentContext({ wallet })
agentContext = getAgentContext({ wallet, registerInstances: [[X509ModuleConfig, new X509ModuleConfig()]] })

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.createAndOpen(agentConfig.walletConfig!)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]'
Expand Down Expand Up @@ -301,13 +302,31 @@ 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)
let trustedCertificates: string[] | undefined

if (certificateChain && x509Config.getTrustedCertificatesForVerification) {
trustedCertificates = await x509Config.getTrustedCertificatesForVerification?.(agentContext, {
certificateChain,
verification: {
type: 'credential',
credential: parsedPresentation,
didcommProofRecordId: proofRecord.id,
},
})
}

if (!trustedCertificates) {
trustedCertificates = 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 (
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/modules/sd-jwt-vc/SdJwtVcOptions.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
Expand Down Expand Up @@ -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[]
}
Loading

0 comments on commit 5f08bc6

Please sign in to comment.