Skip to content

Commit

Permalink
Merge branch 'main' of github.com:openwallet-foundation/credo-ts into…
Browse files Browse the repository at this point in the history
… test
  • Loading branch information
auer-martin committed Oct 8, 2024
2 parents 2fcde60 + 08a485b commit 8728d37
Show file tree
Hide file tree
Showing 17 changed files with 546 additions and 76 deletions.
2 changes: 1 addition & 1 deletion packages/anoncreds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@astronautlabs/jsonpath": "^1.1.2",
"@credo-ts/core": "workspace:*",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/pex-models": "^2.3.1",
"big-integer": "^1.6.51",
"bn.js": "^5.2.1",
"class-transformer": "0.5.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"@sd-jwt/sd-jwt-vc": "^0.7.0",
"@sd-jwt/types": "^0.7.0",
"@sd-jwt/utils": "^0.7.0",
"@sphereon/pex": "^5.0.0-unstable.8",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/pex": "5.0.0-unstable.2",
"@sphereon/pex-models": "^2.3.1",
"@sphereon/ssi-types": "0.29.1-unstable.121",
"@stablelib/ed25519": "^1.0.2",
"@types/ws": "^8.5.4",
Expand Down
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 @@ -22,7 +22,8 @@ import type {
W3CVerifiablePresentation,
} from '@sphereon/ssi-types'

import { PEVersion, PEX, PresentationSubmissionLocation, Status } from '@sphereon/pex'
import { PEVersion, PEX, Status } from '@sphereon/pex'
import { PartialSdJwtDecodedVerifiableCredential } from '@sphereon/pex/dist/main/lib'
import { injectable } from 'tsyringe'

import { Hasher, getJwkFromKey } from '../../crypto'
Expand Down Expand Up @@ -276,9 +277,10 @@ export class DifPresentationExchangeService {
})

return {
verifiablePresentations: verifiablePresentationResultsWithFormat.flatMap((resultWithFormat) =>
resultWithFormat.verifiablePresentationResult.verifiablePresentations.map((encoded) =>
getVerifiablePresentationFromEncoded(agentContext, encoded)
verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) =>
getVerifiablePresentationFromEncoded(
agentContext,
resultWithFormat.verifiablePresentationResult.verifiablePresentation
)
),
presentationSubmission,
Expand Down Expand Up @@ -536,7 +538,9 @@ export class DifPresentationExchangeService {

return signedPresentation.encoded as W3CVerifiablePresentation
} else if (presentationToCreate.claimFormat === ClaimFormat.SdJwtVc) {
const sdJwtInput = presentationInput as SdJwtDecodedVerifiableCredential
const sdJwtInput = presentationInput as
| SdJwtDecodedVerifiableCredential
| PartialSdJwtDecodedVerifiableCredential

if (!domain) {
throw new CredoError("Missing 'domain' property, unable to set required 'aud' property in SD-JWT KB-JWT")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,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 @@ -301,6 +301,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 @@ -126,8 +126,10 @@ export const matrrLaunchpadDraft11JwtVcJson = {
}

export const waltIdDraft11JwtVcJson = {
credentialOffer:
credentialOfferPreAuth:
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%22UniversityDegree%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJlZmMyZjVkZC0wZjQ0LTRmMzgtYTkwMi0zMjA0ZTczMmMzOTEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.OHzYTP_u6I95hHBmjF3RchydGidq3nsT0QHdgJ1AXyR5AFkrTfJwsW4FQIdOdda93uS7FOh_vSVGY0Qngzm7Ag%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D',
credentialOfferAuth:
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%22UniversityDegree%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22efc2f5dd-0f44-4f38-a902-3204e732c391%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJlZmMyZjVkZC0wZjQ0LTRmMzgtYTkwMi0zMjA0ZTczMmMzOTEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.OHzYTP_u6I95hHBmjF3RchydGidq3nsT0QHdgJ1AXyR5AFkrTfJwsW4FQIdOdda93uS7FOh_vSVGY0Qngzm7Ag%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D',
getMetadataResponse: {
issuer: 'https://issuer.portal.walt.id',
authorization_endpoint: 'https://issuer.portal.walt.id/authorize',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe('OpenId4VcHolder', () => {
.post('/credential')
.reply(200, fixture.credentialResponse)

const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer)
const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOfferPreAuth)

await expect(() =>
holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, {
Expand Down Expand Up @@ -286,7 +286,7 @@ describe('OpenId4VcHolder', () => {
.reply(404)

const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer(
fixture.credentialOffer
fixture.credentialOfferAuth
)

const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveIssuanceAuthorizationRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ export class OpenId4VcSiopVerifierService {

const relyingParty = await this.getRelyingParty(agentContext, options.verificationSession.verifierId, {
presentationDefinition: presentationDefinitionsWithLocation?.[0]?.definition,
clientId: requestClientId,
authorizationResponseUrl,
clientId: requestClientId,
})

// This is very unfortunate, but storing state in sphereon's SiOP-OID4VP library
Expand Down 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 @@ -331,10 +332,12 @@ export class OpenId4VcSiopVerifierService {
throw new CredoError('Unable to extract submission from the response.')
}

const vps = Array.isArray(presentations) ? presentations : [presentations]
// FIXME: should return type be an array? As now it doesn't always match the submission
const presentationsArray = Array.isArray(presentations) ? presentations : [presentations]

presentationExchange = {
definition: presentationDefinitions[0].definition,
presentations: vps.map(getVerifiablePresentationFromSphereonWrapped),
presentations: presentationsArray.map(getVerifiablePresentationFromSphereonWrapped),
submission,
}
}
Expand Down Expand Up @@ -459,10 +462,10 @@ export class OpenId4VcSiopVerifierService {
responseMode,
}: {
responseMode?: ResponseMode
authorizationResponseUrl: string
idToken?: boolean
presentationDefinition?: DifPresentationExchangeDefinition
clientId: string
authorizationResponseUrl: string
clientIdScheme?: ClientIdScheme
}
) {
Expand Down Expand Up @@ -519,6 +522,7 @@ export class OpenId4VcSiopVerifierService {
: undefined

builder
.withClientId(clientId)
.withResponseUri(authorizationResponseUrl)
.withIssuer(ResponseIss.SELF_ISSUED_V2)
.withAudience(RequestAud.SELF_ISSUED_V2)
Expand All @@ -541,9 +545,11 @@ export class OpenId4VcSiopVerifierService {

// TODO: we should probably allow some dynamic values here
.withClientMetadata({
client_id: clientId,
...jarmClientMetadata,
client_id_scheme: clientIdScheme,
// FIXME: not passing client_id here means it will not be added
// to the authorization request url (not the signed payload). Need
// to fix that in Sphereon lib
client_id: clientId,
passBy: PassBy.VALUE,
responseTypesSupported: [ResponseType.VP_TOKEN],
subject_syntax_types_supported: supportedDidMethods.map((m) => `did:${m}`),
Expand Down Expand Up @@ -586,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 @@ -616,6 +622,9 @@ export class OpenId4VcSiopVerifierService {
presentation: encodedPresentation,
challenge: options.nonce,
domain: options.audience,
verificationContext: {
openId4VcVerificationSessionId: options.correlationId,
},
})

isValid = verificationResult.isValid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe('OpenId4VcVerifier', () => {
expect(jwt.header.kid).toEqual(verifier.kid)
expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA)
expect(jwt.header.typ).toEqual('JWT')
expect(jwt.payload.additionalClaims.scope).toEqual(undefined)
expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did)
expect(jwt.payload.additionalClaims.response_uri).toEqual(
`http://redirect-uri/${openIdVerifier.verifierId}/authorize`
Expand Down
Loading

0 comments on commit 8728d37

Please sign in to comment.