@@ -6,20 +6,21 @@ import {
66 type StatusListJWTPayload ,
77} from '@sd-jwt/jwt-status-list' ;
88import type { DisclosureFrame , Hasher , Verifier } from '@sd-jwt/types' ;
9- import { base64urlDecode , SDJWTException } from '@sd-jwt/utils' ;
9+ import { SDJWTException } from '@sd-jwt/utils' ;
10+ import z from 'zod' ;
1011import type {
1112 SDJWTVCConfig ,
1213 StatusListFetcher ,
1314 StatusValidator ,
1415} from './sd-jwt-vc-config' ;
1516import type { SdJwtVcPayload } from './sd-jwt-vc-payload' ;
16- import type {
17- Claim ,
18- ClaimPath ,
19- ResolvedTypeMetadata ,
20- TypeMetadataFormat ,
17+ import {
18+ type Claim ,
19+ type ClaimPath ,
20+ type ResolvedTypeMetadata ,
21+ type TypeMetadataFormat ,
22+ TypeMetadataFormatSchema ,
2123} from './sd-jwt-vc-type-metadata-format' ;
22- import type { VcTFetcher } from './sd-jwt-vc-vct' ;
2324import type { VerificationResult } from './verification-result' ;
2425
2526export class SDJwtVcInstance extends SDJwtInstance < SdJwtVcPayload > {
@@ -172,24 +173,24 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
172173 url : string ,
173174 integrity ?: string ,
174175 ) {
175- if ( integrity ) {
176- // validate the integrity of the response according to https://www.w3.org/TR/SRI/
177- const arrayBuffer = await response . arrayBuffer ( ) ;
178- const alg = integrity . split ( '-' ) [ 0 ] ;
179- //TODO: error handling when a hasher is passed that is not supporting the required algorithm according to the spec
180- const hashBuffer = await ( this . userConfig . hasher as Hasher ) (
181- arrayBuffer ,
182- alg ,
176+ if ( ! integrity ) return ;
177+
178+ // validate the integrity of the response according to https://www.w3.org/TR/SRI/
179+ const arrayBuffer = await response . arrayBuffer ( ) ;
180+ const alg = integrity . split ( '-' ) [ 0 ] ;
181+ //TODO: error handling when a hasher is passed that is not supporting the required algorithm according to the spec
182+ const hashBuffer = await ( this . userConfig . hasher as Hasher ) (
183+ arrayBuffer ,
184+ alg ,
185+ ) ;
186+ const integrityHash = integrity . split ( '-' ) [ 1 ] ;
187+ const hash = Array . from ( new Uint8Array ( hashBuffer ) )
188+ . map ( ( byte ) => byte . toString ( 16 ) . padStart ( 2 , '0' ) )
189+ . join ( '' ) ;
190+ if ( hash !== integrityHash ) {
191+ throw new Error (
192+ `Integrity check for ${ url } failed: is ${ hash } , but expected ${ integrityHash } ` ,
183193 ) ;
184- const integrityHash = integrity . split ( '-' ) [ 1 ] ;
185- const hash = Array . from ( new Uint8Array ( hashBuffer ) )
186- . map ( ( byte ) => byte . toString ( 16 ) . padStart ( 2 , '0' ) )
187- . join ( '' ) ;
188- if ( hash !== integrityHash ) {
189- throw new Error (
190- `Integrity check for ${ url } failed: is ${ hash } , but expected ${ integrityHash } ` ,
191- ) ;
192- }
193194 }
194195 }
195196
@@ -198,7 +199,10 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
198199 * @param url
199200 * @returns
200201 */
201- private async fetch < T > ( url : string , integrity ?: string ) : Promise < T > {
202+ private async fetchWithIntegrity (
203+ url : string ,
204+ integrity ?: string ,
205+ ) : Promise < unknown > {
202206 try {
203207 const response = await fetch ( url , {
204208 signal : AbortSignal . timeout ( this . userConfig . timeout ?? 10000 ) ,
@@ -210,7 +214,9 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
210214 ) ;
211215 }
212216 await this . validateIntegrity ( response . clone ( ) , url , integrity ) ;
213- return response . json ( ) as Promise < T > ;
217+ const data = await response . json ( ) ;
218+
219+ return data ;
214220 } catch ( error ) {
215221 if ( ( error as Error ) . name === 'TimeoutError' ) {
216222 throw new Error ( `Request to ${ url } timed out` ) ;
@@ -228,7 +234,10 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
228234 private async fetchVct (
229235 result : VerificationResult ,
230236 ) : Promise < ResolvedTypeMetadata | undefined > {
231- const typeMetadataFormat = await this . fetchSingleVct ( result ) ;
237+ const typeMetadataFormat = await this . fetchSingleVct (
238+ result . payload . vct ,
239+ result . payload [ 'vct#integrity' ] ,
240+ ) ;
232241
233242 if ( ! typeMetadataFormat ) return undefined ;
234243
@@ -405,12 +414,7 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
405414 // Mark this VCT as visited
406415 visitedVcts . add ( parentTypeMetadata . extends ) ;
407416
408- // Fetch the type metadata
409- const fetcher : VcTFetcher =
410- this . userConfig . vctFetcher ??
411- ( ( uri , integrity ) => this . fetch ( uri , integrity ) ) ;
412-
413- const extendedTypeMetadata = await fetcher (
417+ const extendedTypeMetadata = await this . fetchSingleVct (
414418 parentTypeMetadata . extends ,
415419 parentTypeMetadata [ 'extends#integrity' ] ,
416420 ) ;
@@ -459,59 +463,30 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
459463 }
460464
461465 /**
462- * Fetches VCT Metadata of the SD-JWT-VC. Returns the type metadata format. If the SD-JWT-VC does not contain a vct claim, an error is thrown .
466+ * Fetches and verifies the VCT Metadata for a VCT value .
463467 * @param result
464468 * @returns
465469 */
466470 private async fetchSingleVct (
467- result : VerificationResult ,
471+ vct : string ,
472+ integrity ?: string ,
468473 ) : Promise < TypeMetadataFormat | undefined > {
469- if ( ! result . payload . vct ) {
470- throw new SDJWTException ( 'vct claim is required' ) ;
471- }
472-
473- if ( result . header ?. vctm ) {
474- return this . fetchVctFromHeader ( result . payload . vct , result ) ;
475- }
476-
477- const fetcher : VcTFetcher =
474+ const fetcher =
478475 this . userConfig . vctFetcher ??
479- ( ( uri , integrity ) => this . fetch ( uri , integrity ) ) ;
480- return fetcher ( result . payload . vct , result . payload [ 'vct#integrity' ] ) ;
481- }
482-
483- /**
484- * Fetches VCT Metadata from the header of the SD-JWT-VC. Returns the type metadata format. If the SD-JWT-VC does not contain a vct claim, an error is thrown.
485- * @param result
486- * @param
487- */
488- private async fetchVctFromHeader (
489- vct : string ,
490- result : VerificationResult ,
491- ) : Promise < TypeMetadataFormat > {
492- const vctmHeader = result . header ?. vctm ;
493-
494- if ( ! vctmHeader || ! Array . isArray ( vctmHeader ) ) {
495- throw new Error ( 'vctm claim in SD JWT header is invalid' ) ;
496- }
497-
498- const typeMetadataFormat = ( vctmHeader as unknown [ ] )
499- . map ( ( vctm ) => {
500- if ( ! ( typeof vctm === 'string' ) ) {
501- throw new Error ( 'vctm claim in SD JWT header is invalid' ) ;
502- }
476+ ( ( uri , integrity ) => this . fetchWithIntegrity ( uri , integrity ) ) ;
503477
504- return JSON . parse ( base64urlDecode ( vctm ) ) ;
505- } )
506- . find ( ( typeMetadataFormat ) => {
507- return typeMetadataFormat . vct === vct ;
508- } ) ;
478+ // Data may be undefined
479+ const data = await fetcher ( vct , integrity ) ;
480+ if ( ! data ) return undefined ;
509481
510- if ( ! typeMetadataFormat ) {
511- throw new Error ( 'could not find VCT Metadata in JWT header' ) ;
482+ const validated = TypeMetadataFormatSchema . safeParse ( data ) ;
483+ if ( ! validated . success ) {
484+ throw new SDJWTException (
485+ `Invalid VCT type metadata for vct '${ vct } ':\n${ z . prettifyError ( validated . error ) } ` ,
486+ ) ;
512487 }
513488
514- return typeMetadataFormat ;
489+ return validated . data ;
515490 }
516491
517492 /**
0 commit comments