import { PresentationValidator } from '../presentationValidatorFactory.js'; import { ValidationResult } from '../../shared/dto/validationResult.dto.js'; import { CredentialFormat, VerifiablePresentation, VPTokenData, } from './vpTokenCredentialsExtractor.js'; import { VerifiableCredentialsValidator } from '../credential/verifiableCredentialsValidator.js'; import { JsonCredential } from '../../shared/dto/jsonCredential.dto.js'; import { CredentialValidatorFactory } from '../credentialValidatorFactory.js'; import { PresentationValidationOptions } from '../../shared/dto/validationOptions.dto.js'; import { DEFAULT_ALL_CONTEXT, DEFAULT_PRESENTATION_TYPE, } from '../../config.js'; import joseWrapper from '../../shared/middleware/joseWrapper.js'; import { SignatureValidator } from '../credential/signatureValidator.js'; import { DidPublicKeyResolver } from '../../resolvers/didPublicKeyResolver.js'; import { DidDocumentResolver } from '../../resolvers/didDocumentResolver.js'; import { CredentialValidationTypes } from './verifiablePresentationValidationReport.js'; import { HttpPublicKeyResolver } from '../../resolvers/httpPublicKeyResolver.js'; import { DcqlQueryValidator } from './dcqlQueryValidator.js'; import { JsonPresentation } from '../../shared/dto/jsonPresentation.dto.js'; import { DcqlPresentation } from './dcqlPresentation.js'; import { DataIntegrityProofValidator } from '../credential/dataIntegrityProofValidator.js'; export class VerifiablePresentationValidator implements PresentationValidator { constructor( private options: PresentationValidationOptions, private vpTokenData: VPTokenData, private publicKeyResolver: DidPublicKeyResolver, private signatureValidator: SignatureValidator, ) {} async validate( presentation: DcqlPresentation, audience: string, ): Promise { let messages: string[] = []; if (this.options?.dcqlQuery) { const dcqlResult = await this.validateDcqlQuery( presentation as object, audience, ); if (!dcqlResult.valid) { if (dcqlResult.messages?.length) { messages = messages.concat(dcqlResult.messages); } if ( this.options?.report && this.options?.verifiablePresentationValidationReport ) { this.options.verifiablePresentationValidationReport.updateFailedPresentation( messages, ); } return { valid: false, ...(messages.length !== 0 && { messages }), }; } } let globalValidationResult = true; for (const [index] of this.vpTokenData.verifiableCredentials.entries()) { const credentialValidationResult = await this.validateVerifiableCredentials(index); if (!credentialValidationResult.valid) { globalValidationResult = false; messages = messages.concat(credentialValidationResult.messages); } } if ( messages.length !== 0 && this.options?.report && this.options?.verifiablePresentationValidationReport ) { this.options.verifiablePresentationValidationReport.updateFailedPresentation( messages, ); } if (messages.length !== 0 && !this.options?.report) { return { valid: false, messages }; } return { valid: globalValidationResult, ...(messages.length !== 0 && { messages }), }; } private async validateDcqlQuery( presentation: object, audience?: string, ): Promise { const result = new DcqlQueryValidator( this.vpTokenData.verifiableCredentials, this.options.dcqlQuery, this.vpTokenData.credentialQueryIds, ).validate(); if (!result.valid) { return { valid: false, messages: [result.message] }; } for (const credentialQuery of this.options.dcqlQuery.credentials) { const vp = presentation[credentialQuery.id] as Array; for (const presentation of vp) { if (typeof presentation === 'string' && !presentation.includes('~')) { // JWT VP — validate via the existing JWS path const presentationValidationResult = await this.validatePresentation( presentation, audience, ); if (!presentationValidationResult?.valid) { return { valid: false, messages: presentationValidationResult?.messages, }; } continue; } if (typeof presentation === 'object' && presentation !== null) { const jsonPres = presentation as JsonPresentation; if (jsonPres.proof?.jws) { // VCDM 1.0 JSON-LD VP — validate via the existing JWS path const presentationValidationResult = await this.validatePresentation( jsonPres.proof.jws, audience, ); if (!presentationValidationResult?.valid) { return { valid: false, messages: presentationValidationResult?.messages, }; } } // VCDM 2.0 DataIntegrityProof VPs: proof is validated implicitly when // the embedded credentials are verified below. A dedicated VP-level // DataIntegrityProof check can be added here in a future iteration. } } } return { valid: true }; } private async validateVerifiableCredentials( index: number, ): Promise { const credentialFormatTuple = this.vpTokenData.verifiableCredentials[index]; const decodedEntry = this.vpTokenData.verifiableCredentialsDecoded[index]; // `decodedEntry` is either: // - a decoded JWT payload → { vc: {...}, iss: '...', ... } // - a VCDM 2.0 wrapper → { vc: JsonCredential, iss: '...' } const checkResult = this.checkCredentialIssuer(decodedEntry?.vc); if (!checkResult) { const credentialId = decodedEntry?.vc?.jti ?? decodedEntry?.vc?.id; const messages = [ `Presentation issuer does not match with credential holder. Credential Id: ${credentialId}`, ]; this.options.verifiablePresentationValidationReport?.updateFailedPresentation( messages, credentialId, [ CredentialValidationTypes.Expiration, CredentialValidationTypes.NotYetValid, CredentialValidationTypes.Revocation, CredentialValidationTypes.CredentialSignature, ], ); return Promise.resolve({ valid: false, messages: messages, }); } if (credentialFormatTuple.format === CredentialFormat.JSON) { const jsonCredential = credentialFormatTuple.verifiableCredential as JsonCredential; if (jsonCredential.proof?.proofValue && !jsonCredential.proof?.jws) { // VCDM 2.0 DataIntegrityProof credential const dataIntegrityValidator = new DataIntegrityProofValidator( new DidDocumentResolver(), ); return dataIntegrityValidator.validate(jsonCredential, { didRegistry: this.options.didRegistry, ...(this.options.report && { report: this.options.report }), }); } // VCDM 1.0 JSON-LD credential with JWS proof const verifiableCredentialsValidator = new VerifiableCredentialsValidator( new DidPublicKeyResolver(new DidDocumentResolver()), new HttpPublicKeyResolver(), new SignatureValidator(), this.options?.verifiablePresentationValidationReport, ); return verifiableCredentialsValidator.validate(jsonCredential.proof.jws, { didRegistry: this.options.didRegistry, ...(this.options.report && { report: this.options.report }), }); } const credentialsValidator = CredentialValidatorFactory.create( decodedEntry.iss, ); return credentialsValidator.validate( credentialFormatTuple.verifiableCredential as string, { didRegistry: this.options.didRegistry, ebsiAuthority: this.options.ebsiAuthority, }, ); } private checkCredentialIssuer(verifiableCredential: JsonCredential) { if ( verifiableCredential.hasOwnProperty('credentialSubject') && verifiableCredential.credentialSubject.hasOwnProperty('id') ) { return ( verifiableCredential.credentialSubject.id === this.vpTokenData.vpTokenIssuer ); } return false; } private async validatePresentation( presentation: string, audience: string, ): Promise { let validationResult: ValidationResult; const decodedPresentation = joseWrapper.decodeJWT(presentation); validationResult = this.validateAudience(decodedPresentation, audience); if (!validationResult.valid) { return validationResult; } validationResult = this.validatePresentationStructure(decodedPresentation); if (!validationResult.valid) { return validationResult; } const decodedJwtHeader = joseWrapper.decodeJwtProtectedHeader(presentation); const issuerKid: string = decodedJwtHeader.kid; const publicKey = await this.publicKeyResolver.getPublicKeyJwk( issuerKid, this.options, ); const signatureValidatorResult = await this.signatureValidator.validate( presentation, publicKey, decodedJwtHeader.alg, 'Verifiable presentation', ); this.options.verifiablePresentationValidationReport?.updatePresentationInformation( signatureValidatorResult, ); return signatureValidatorResult; } private validateAudience(vp: VerifiablePresentation, audience: string) { //TODO: case where aud is missing in the VP if (vp?.aud !== audience) { const messages = [ `ValidationError: JWT "aud" property MUST match the expected audience. Presentation: ${vp.vp.id}`, ]; this.options.verifiablePresentationValidationReport?.updateFailedPresentation( messages, ); return { valid: false, messages, }; } return { valid: true, messages: [], }; } private validatePresentationStructure( decodedPresentation: VerifiablePresentation, ) { let messages: string[]; if ( !decodedPresentation || !decodedPresentation.vp || !decodedPresentation.vp['@context'] || !decodedPresentation.vp.type || !decodedPresentation.vp.verifiableCredential ) { messages = [ `ValidationError: Presentation is missing parameters. Required: @context, type and verifiableCredential. Presentation: ${decodedPresentation.vp.id}`, ]; this.options.verifiablePresentationValidationReport?.updateFailedPresentation( messages, ); return { valid: false, messages, }; } if (decodedPresentation.vp['@context'].length < 1) { (messages = [ `ValidationError: @context is missing default context ${DEFAULT_ALL_CONTEXT}. Presentation: ${decodedPresentation.vp.id}`, ]), this.options.verifiablePresentationValidationReport?.updateFailedPresentation( messages, ); return { valid: false, messages, }; } if ( decodedPresentation.vp.type.length < 1 || !decodedPresentation.vp.type.includes(DEFAULT_PRESENTATION_TYPE) ) { messages = [ `type is missing default "${DEFAULT_PRESENTATION_TYPE}". Presentation: ${decodedPresentation.vp.id}`, ]; this.options.verifiablePresentationValidationReport?.updateFailedPresentation( messages, ); return { valid: false, messages: messages, }; } if (decodedPresentation.vp.verifiableCredential.length < 1) { messages = [ `vp.verifiableCredential must not be empty. Presentation: ${decodedPresentation.vp.id}`, ]; this.options.verifiablePresentationValidationReport?.updateFailedPresentation( messages, ); return { valid: false, messages, }; } if ( decodedPresentation.exp && new Date((decodedPresentation.exp as number) * 1000) < new Date() ) { (messages = [ `Verifiable Presentation is expired. Presentation: ${decodedPresentation.vp.id}`, ]), this.options.verifiablePresentationValidationReport?.updateFailedPresentation( messages, ); return { valid: false, messages, }; } return { valid: true, messages: [], }; } }