From b742fbe5931813b705ad376bf8a5ccf98c011cdf Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Tue, 3 Dec 2024 00:55:48 +0100 Subject: [PATCH] feat: Validation improvements --- .../src/agent/CredentialValidation.ts | 105 ++++++++++------- packages/oid4vci-issuer/src/functions.ts | 108 +++++++++++------- 2 files changed, 127 insertions(+), 86 deletions(-) diff --git a/packages/credential-validation/src/agent/CredentialValidation.ts b/packages/credential-validation/src/agent/CredentialValidation.ts index 0a8dbb61c..7e6abfa5e 100644 --- a/packages/credential-validation/src/agent/CredentialValidation.ts +++ b/packages/credential-validation/src/agent/CredentialValidation.ts @@ -1,4 +1,21 @@ -import { IAgentPlugin, IVerifyCredentialArgs, W3CVerifiableCredential as VeramoW3CVerifiableCredential } from '@veramo/core' +import { com } from '@sphereon/kmp-mdoc-core' +import { IVerifySdJwtVcResult } from '@sphereon/ssi-sdk.sd-jwt' +import { + CredentialMapper, + ICoseKeyJson, + ICredentialSchemaType, + IVerifyResult, + OriginalVerifiableCredential, + WrappedVerifiableCredential +} from '@sphereon/ssi-types' +import { + IAgentPlugin, + IVerifyCredentialArgs, + W3CVerifiableCredential as VeramoW3CVerifiableCredential +} from '@veramo/core' +import addFormats from 'ajv-formats' +import Ajv2020 from 'ajv/dist/2020' +import fetch from 'cross-fetch' import { CredentialVerificationError, ICredentialValidation, @@ -10,24 +27,11 @@ import { VerificationSubResult, VerifyCredentialArgs, VerifyMdocCredentialArgs, - VerifySDJWTCredentialArgs, + VerifySDJWTCredentialArgs } from '../index' -import { - CredentialMapper, - ICoseKeyJson, - ICredentialSchemaType, - IVerifyResult, - OriginalVerifiableCredential, - WrappedVerifiableCredential, -} from '@sphereon/ssi-types' -import fetch from 'cross-fetch' -import Ajv2020 from 'ajv/dist/2020' -import addFormats from 'ajv-formats' -import { com } from '@sphereon/kmp-mdoc-core' -import { IVerifySdJwtVcResult } from '@sphereon/ssi-sdk.sd-jwt' +import IVerifySignatureResult = com.sphereon.crypto.generic.IVerifySignatureResult import decodeFrom = com.sphereon.kmp.decodeFrom import IssuerSignedCbor = com.sphereon.mdoc.data.device.IssuerSignedCbor -import IVerifySignatureResult = com.sphereon.crypto.generic.IVerifySignatureResult // Exposing the methods here for any REST implementation export const credentialValidationMethods: Array = [ @@ -35,7 +39,7 @@ export const credentialValidationMethods: Array = [ 'cvVerifySchema', 'cvVerifyMdoc', 'cvVerifySDJWTCredential', - 'cvVerifyW3CCredential', + 'cvVerifyW3CCredential' ] /** @@ -48,7 +52,7 @@ export class CredentialValidation implements IAgentPlugin { cvVerifySchema: this.cvVerifySchema.bind(this), cvVerifyMdoc: this.cvVerifyMdoc.bind(this), cvVerifySDJWTCredential: this.cvVerifySDJWTCredential.bind(this), - cvVerifyW3CCredential: this.cvVerifyW3CCredential.bind(this), + cvVerifyW3CCredential: this.cvVerifyW3CCredential.bind(this) } private detectSchemas(wrappedVC: WrappedVerifiableCredential): ICredentialSchemaType[] | undefined { @@ -75,7 +79,7 @@ export class CredentialValidation implements IAgentPlugin { const schemaResult = await this.cvVerifySchema({ credential, validationPolicy: policies?.schemaValidation ?? SchemaValidation.WHEN_PRESENT, - hasher, + hasher }) if (!schemaResult.result) { return schemaResult @@ -85,7 +89,10 @@ export class CredentialValidation implements IAgentPlugin { } else if (CredentialMapper.isSdJwtEncoded(credential)) { return await this.cvVerifySDJWTCredential({ credential, hasher }, context) } else { - return await this.cvVerifyW3CCredential({ ...args, credential: credential as VeramoW3CVerifiableCredential }, context) + return await this.cvVerifyW3CCredential({ + ...args, + credential: credential as VeramoW3CVerifiableCredential + }, context) } } @@ -96,7 +103,7 @@ export class CredentialValidation implements IAgentPlugin { return { result: true, source: wrappedCredential, - subResults: [], + subResults: [] } } return this.validateSchema(wrappedCredential, validationPolicy) @@ -105,17 +112,20 @@ export class CredentialValidation implements IAgentPlugin { private async validateSchema(wrappedVC: WrappedVerifiableCredential, validationPolicy?: SchemaValidation): Promise { const schemas: ICredentialSchemaType[] | undefined = this.detectSchemas(wrappedVC) if (!schemas) { - return validationPolicy === SchemaValidation.ALWAYS - ? { - result: false, - source: wrappedVC, - subResults: [], - } - : { - result: true, - source: wrappedVC, - subResults: [], - } + if (validationPolicy === SchemaValidation.ALWAYS) { + console.error(`No schema found for credential, but validation policy is set to ALWAYS. Returning false. Credential: ${JSON.stringify(wrappedVC.credential, null, 2)}`) + return { + result: false, + source: wrappedVC, + subResults: [] + } + } else { + return { + result: true, + source: wrappedVC, + subResults: [] + } + } } const subResults: VerificationSubResult[] = await Promise.all(schemas.map((schema) => this.verifyCredentialAgainstSchema(wrappedVC, schema))) @@ -123,7 +133,7 @@ export class CredentialValidation implements IAgentPlugin { return { result: subResults.every((subResult) => subResult.result), source: wrappedVC, - subResults, + subResults } } @@ -140,10 +150,11 @@ export class CredentialValidation implements IAgentPlugin { let schemaValue try { schemaValue = await this.fetchSchema(schemaUrl) - } catch (e) { + } catch (error) { + console.error(error) return { result: false, - error: e, + error: error } } @@ -152,8 +163,11 @@ export class CredentialValidation implements IAgentPlugin { const validate = await ajv.compileAsync(schemaValue) const valid = validate(wrappedVC.credential) + if (!valid) { + console.error(`Schema validation failed for `, wrappedVC.credential) + } return { - result: valid, + result: valid } } @@ -163,11 +177,12 @@ export class CredentialValidation implements IAgentPlugin { const issuerSigned = IssuerSignedCbor.Static.cborDecode(decodeFrom(credential, com.sphereon.kmp.Encoding.BASE64URL)) const verification = await context.agent.mdocVerifyIssuerSigned({ input: issuerSigned.toJson().issuerAuth }).catch((error: Error) => { + console.error(error) return { name: 'mdoc', critical: true, error: true, - message: error.message ?? 'SD-JWT VC could not be verified', + message: error.message ?? 'Mdoc Issuer Signed VC could not be verified' } satisfies IVerifySignatureResult }) @@ -176,8 +191,8 @@ export class CredentialValidation implements IAgentPlugin { result: !verification.error, subResults: [], ...(verification.error && { - error: verification.message ?? `Could not verify mdoc from issuer`, - }), + error: verification.message ?? `Could not verify mdoc from issuer` + }) } } @@ -194,9 +209,9 @@ export class CredentialValidation implements IAgentPlugin { result, ...(!result && { error: 'Invalid JWT VC', - errorDetails: `JWT VC was not valid with policies: ${JSON.stringify(policies)}`, + errorDetails: `JWT VC was not valid with policies: ${JSON.stringify(policies)}` }), - subResults: [], + subResults: [] } } else { // TODO look at what this is doing and make it simple and readable @@ -213,6 +228,7 @@ export class CredentialValidation implements IAgentPlugin { (errorDetails !== '' ? `${errorDetails}, ` : '') + result.error?.errors?.map((error) => (error?.details?.code ? `${error.details.code}, ` : '') + (error?.details?.url ?? '')).join(', ') } + console.error(error) } return { @@ -220,7 +236,7 @@ export class CredentialValidation implements IAgentPlugin { result: result.verified, subResults, error, - errorDetails, + errorDetails } } } @@ -231,9 +247,10 @@ export class CredentialValidation implements IAgentPlugin { const verification: IVerifySdJwtVcResult | CredentialVerificationError = await context.agent .verifySdJwtVc({ credential }) .catch((error: Error): CredentialVerificationError => { + console.error(error) return { error: 'Invalid SD-JWT VC', - errorDetails: error.message ?? 'SD-JWT VC could not be verified', + errorDetails: error.message ?? 'SD-JWT VC could not be verified' } }) @@ -242,7 +259,7 @@ export class CredentialValidation implements IAgentPlugin { source: CredentialMapper.toWrappedVerifiableCredential(credential as OriginalVerifiableCredential, { hasher }), result, subResults: [], - ...(!result && { ...verification }), + ...(!result && { ...verification }) } } } diff --git a/packages/oid4vci-issuer/src/functions.ts b/packages/oid4vci-issuer/src/functions.ts index 19e5d2359..91f75529c 100644 --- a/packages/oid4vci-issuer/src/functions.ts +++ b/packages/oid4vci-issuer/src/functions.ts @@ -4,29 +4,38 @@ import { IssuerMetadata, Jwt, JwtVerifyResult, - OID4VCICredentialFormat, + OID4VCICredentialFormat } from '@sphereon/oid4vci-common' -import { CredentialDataSupplier, CredentialIssuanceInput, CredentialSignerCallback, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' +import { JWTHeader, JWTPayload } from '@sphereon/oid4vci-common/lib/types' +import { + CredentialDataSupplier, + CredentialIssuanceInput, + CredentialSignerCallback, + VcIssuer, + VcIssuerBuilder +} from '@sphereon/oid4vci-issuer' import { getAgentResolver, IDIDOptions } from '@sphereon/ssi-sdk-ext.did-utils' -import { legacyKeyRefsToIdentifierOpts, ManagedIdentifierOptsOrResult } from '@sphereon/ssi-sdk-ext.identifier-resolution' +import { + legacyKeyRefsToIdentifierOpts, + ManagedIdentifierOptsOrResult +} from '@sphereon/ssi-sdk-ext.identifier-resolution' import { contextHasPlugin } from '@sphereon/ssi-sdk.agent-config' +import { SdJwtVcPayload } from '@sphereon/ssi-sdk.sd-jwt/dist' import { IStatusListPlugin } from '@sphereon/ssi-sdk.vc-status-list' import { CompactSdJwtVc, CredentialMapper, ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' import { CredentialPayload, DIDDocument, ProofFormat } from '@veramo/core' import { bytesToBase64 } from '@veramo/utils' import { createJWT, decodeJWT, JWTVerifyOptions, verifyJWT } from 'did-jwt' import { Resolvable } from 'did-resolver' -import { IIssuerOptions, IRequiredContext } from './types/IOID4VCIIssuer' -import { SdJwtVcPayload } from '@sphereon/ssi-sdk.sd-jwt/dist' import jwtDecode from 'jwt-decode' -import { JWTHeader, JWTPayload } from '@sphereon/oid4vci-common/lib/types' +import { IIssuerOptions, IRequiredContext } from './types/IOID4VCIIssuer' export function getJwtVerifyCallback({ verifyOpts }: { verifyOpts?: JWTVerifyOptions }, _context: IRequiredContext) { return async (args: { jwt: string; kid?: string }): Promise> => { const resolver = getAgentResolver(_context, { resolverResolution: true, uniresolverResolution: true, - localResolution: true, + localResolution: true }) verifyOpts = { ...verifyOpts, resolver: verifyOpts?.resolver } // Resolver separately as that is a function if (!verifyOpts?.resolver || typeof verifyOpts?.resolver?.resolve !== 'function') { @@ -52,34 +61,44 @@ export function getJwtVerifyCallback({ verifyOpts }: { verifyOpts?: JWTVerifyOpt return { alg, ...{ identifier }, - jwt: { header, payload }, + jwt: { header, payload } } as JwtVerifyResult - } else { - const result = await verifyJWT(args.jwt, verifyOpts) - if (!result.verified) { - console.log(`JWT invalid: ${args.jwt}`) - throw Error('JWT did not verify successfully') - } - const decodedJwt = (await decodeJWT(args.jwt)) as Jwt - const kid = args.kid ?? decodedJwt.header.kid - if (!kid) { - throw Error('No kid value found') - } - const did = kid.split('#')[0] - const didResolution = await resolver.resolve(did) - if (!didResolution || !didResolution.didDocument) { - throw Error(`Could not resolve did: ${did}, metadata: ${didResolution?.didResolutionMetadata}`) - } + } + + + const decodedJwt = (await decodeJWT(args.jwt)) as Jwt + const kid = args.kid ?? decodedJwt.header.kid - const alg = decodedJwt.header.alg + if (!kid || !kid.startsWith('did:')) { + // No DID method present in header. We already performed the validation above. So return that return { - alg, - kid, - did, - didDocument: didResolution.didDocument, - jwt: decodedJwt, - } + alg: decodedJwt.header.alg, + jwt: decodedJwt + } as JwtVerifyResult + } + const did = kid.split('#')[0] + + + const didResult = await verifyJWT(args.jwt, verifyOpts) + if (!didResult.verified) { + console.log(`JWT invalid: ${args.jwt}`) + throw Error('JWT did not verify successfully') + } + + const didResolution = await resolver.resolve(did) + if (!didResolution || !didResolution.didDocument) { + throw Error(`Could not resolve did: ${did}, metadata: ${didResolution?.didResolutionMetadata}`) + } + + const alg = decodedJwt.header.alg + return { + alg, + kid, + did, + didDocument: didResolution.didDocument, + jwt: decodedJwt } + } } @@ -102,7 +121,7 @@ export async function getAccessTokenKeyRef( */ didOpts?: IDIDOptions }, - context: IRequiredContext, + context: IRequiredContext ) { let identifier = legacyKeyRefsToIdentifierOpts(opts) return await context.agent.identifierManagedGet(identifier) @@ -127,7 +146,7 @@ export async function getAccessTokenSignerCallback( */ didOpts?: IDIDOptions }, - context: IRequiredContext, + context: IRequiredContext ) { const signer = async (data: string | Uint8Array) => { let dataString, encoding: 'base64' | undefined @@ -162,7 +181,7 @@ export async function getCredentialSignerCallback( idOpts: ManagedIdentifierOptsOrResult & { crypto?: Crypto }, - context: IRequiredContext, + context: IRequiredContext ): Promise> { async function issueVCCallback(args: { credentialRequest: CredentialRequest @@ -209,7 +228,7 @@ export async function getCredentialSignerCallback( removeOriginalFields: false, fetchRemoteContexts: true, domain: typeof credential.issuer === 'object' ? credential.issuer.id : credential.issuer, - ...(resolution.kid && { header: { kid: resolution.kid } }), + ...(resolution.kid && { header: { kid: resolution.kid } }) }) return (proofFormat === 'jwt' && 'jwt' in result.proof ? result.proof.jwt : result) as W3CVerifiableCredential } else if (CredentialMapper.isSdJwtDecodedCredentialPayload(credential)) { @@ -227,13 +246,13 @@ export async function getCredentialSignerCallback( delete credential['disclosureFrame'] } else { disclosureFrame = { - _sd: credential['_sd'], + _sd: credential['_sd'] } } const result = await context.agent.createSdJwtVc({ credentialPayload: sdJwtPayload, disclosureFrame: disclosureFrame, - resolution, + resolution }) return result.credential } /*else if (CredentialMapper.isMsoMdocDecodedCredential(credential)) { @@ -253,7 +272,7 @@ export async function createVciIssuerBuilder( resolver?: Resolvable credentialDataSupplier?: CredentialDataSupplier }, - context: IRequiredContext, + context: IRequiredContext ): Promise> { const { issuerOpts, issuerMetadata, authorizationServerMetadata } = args @@ -272,7 +291,7 @@ export async function createVciIssuerBuilder( ...issuerOpts?.didOpts?.resolveOpts?.jwtVerifyOpts, ...args?.issuerOpts?.resolveOpts?.jwtVerifyOpts, resolver, - audience: issuerMetadata.credential_issuer as string, // FIXME legacy version had {display: NameAndLocale | NameAndLocale[]} as credential_issuer + audience: issuerMetadata.credential_issuer as string // FIXME legacy version had {display: NameAndLocale | NameAndLocale[]} as credential_issuer } builder.withIssuerMetadata(issuerMetadata) builder.withAuthorizationMetadata(authorizationServerMetadata) @@ -295,14 +314,19 @@ export async function createVciIssuer( issuerOpts, issuerMetadata, authorizationServerMetadata, - credentialDataSupplier, + credentialDataSupplier }: { issuerOpts: IIssuerOptions issuerMetadata: IssuerMetadata authorizationServerMetadata: AuthorizationServerMetadata credentialDataSupplier?: CredentialDataSupplier }, - context: IRequiredContext, + context: IRequiredContext ): Promise> { - return (await createVciIssuerBuilder({ issuerOpts, issuerMetadata, authorizationServerMetadata, credentialDataSupplier }, context)).build() + return (await createVciIssuerBuilder({ + issuerOpts, + issuerMetadata, + authorizationServerMetadata, + credentialDataSupplier + }, context)).build() }