Skip to content

Commit

Permalink
docs(changeset): feat: allow dynamicaly providing x509 certificates f…
Browse files Browse the repository at this point in the history
…or all types of verifications

Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Nov 26, 2024
1 parent 3977946 commit 87a8f01
Show file tree
Hide file tree
Showing 19 changed files with 331 additions and 93 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
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
30 changes: 20 additions & 10 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,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) {
Expand Down
31 changes: 24 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,30 @@ 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
// 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.')
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
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,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 (
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[]
}
29 changes: 24 additions & 5 deletions packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -191,7 +191,7 @@ export class SdJwtVcService {

public async verify<Header extends SdJwtVcHeader = SdJwtVcHeader, Payload extends SdJwtVcPayload = SdJwtVcPayload>(
agentContext: AgentContext,
{ compactSdJwtVc, keyBinding, requiredClaimKeys, fetchTypeMetadata }: SdJwtVcVerifyOptions
{ compactSdJwtVc, keyBinding, requiredClaimKeys, fetchTypeMetadata, trustedCertificates }: SdJwtVcVerifyOptions
): Promise<
| { isValid: true; verification: VerificationResult; sdJwtVc: SdJwtVc<Header, Payload> }
| { isValid: false; verification: VerificationResult; sdJwtVc?: SdJwtVc<Header, Payload>; error: Error }
Expand Down Expand Up @@ -229,7 +229,12 @@ export class SdJwtVcService {
} satisfies SdJwtVc<Header, Payload>

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
Expand Down Expand Up @@ -455,8 +460,11 @@ export class SdJwtVcService {

private async parseIssuerFromCredential<Header extends SdJwtVcHeader, Payload extends SdJwtVcPayload>(
agentContext: AgentContext,
sdJwtVc: SDJwt<Header, Payload>
sdJwtVc: SDJwt<Header, Payload>,
credoSdJwtVc: SdJwtVc<Header, Payload>,
_trustedCertificates?: EncodedX509Certificate[]
): Promise<SdJwtVcIssuer> {
const x509Config = agentContext.dependencyManager.resolve(X509ModuleConfig)
if (!sdJwtVc.jwt?.payload) {
throw new SdJwtVcError('Credential not exist')
}
Expand All @@ -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.'
Expand Down
16 changes: 2 additions & 14 deletions packages/core/src/modules/vc/W3cCredentialServiceOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 | ClaimFormat.LdpVc | undefined = undefined> =
Format extends ClaimFormat.JwtVc
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 87a8f01

Please sign in to comment.