Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow dynamicaly providing x509 certificates for all types of verifications #2112

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading