Skip to content

Commit f89ba44

Browse files
authored
feat: fetch VCT Metadata from SD JWT header (#288) (#294)
Signed-off-by: Pascal Knoth <pascal@malach.it>
1 parent 1eefb26 commit f89ba44

File tree

2 files changed

+70
-10
lines changed

2 files changed

+70
-10
lines changed

packages/sd-jwt-vc/src/sd-jwt-vc-instance.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Jwt, SDJwt, SDJwtInstance, type VerifierOptions } from '@sd-jwt/core';
22
import type { DisclosureFrame, Hasher, Verifier } from '@sd-jwt/types';
3-
import { SDJWTException } from '@sd-jwt/utils';
3+
import { base64urlDecode, SDJWTException } from '@sd-jwt/utils';
44
import type { SdJwtVcPayload } from './sd-jwt-vc-payload';
55
import type {
66
SDJWTVCConfig,
@@ -294,12 +294,50 @@ export class SDJwtVcInstance extends SDJwtInstance<SdJwtVcPayload> {
294294
throw new SDJWTException('vct claim is required');
295295
}
296296

297+
if (result.header?.vctm) {
298+
return this.fetchVctFromHeader(result.payload.vct, result);
299+
}
300+
297301
const fetcher: VcTFetcher =
298302
this.userConfig.vctFetcher ??
299303
((uri, integrity) => this.fetch(uri, integrity));
300304
return fetcher(result.payload.vct, result.payload['vct#Integrity']);
301305
}
302306

307+
/**
308+
* 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.
309+
* @param result
310+
* @param
311+
*/
312+
private async fetchVctFromHeader(
313+
vct: string,
314+
result: VerificationResult,
315+
): Promise<TypeMetadataFormat> {
316+
const vctmHeader = result.header?.vctm;
317+
318+
if (!vctmHeader || !Array.isArray(vctmHeader)) {
319+
throw new Error('vctm claim in SD JWT header is invalid');
320+
}
321+
322+
const typeMetadataFormat = (vctmHeader as unknown[])
323+
.map((vctm) => {
324+
if (!(typeof vctm === 'string')) {
325+
throw new Error('vctm claim in SD JWT header is invalid');
326+
}
327+
328+
return JSON.parse(base64urlDecode(vctm));
329+
})
330+
.find((typeMetadataFormat) => {
331+
return typeMetadataFormat.vct === vct;
332+
});
333+
334+
if (!typeMetadataFormat) {
335+
throw new Error('could not find VCT Metadata in JWT header');
336+
}
337+
338+
return typeMetadataFormat;
339+
}
340+
303341
/**
304342
* Verifies the status of the SD-JWT-VC.
305343
* @param result

packages/sd-jwt-vc/src/test/vct.spec.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import { HttpResponse, http } from 'msw';
99
import { afterEach } from 'node:test';
1010
import type { TypeMetadataFormat } from '../sd-jwt-vc-type-metadata-format';
1111

12+
const exampleVctm = {
13+
vct: 'http://example.com/example',
14+
name: 'ExampleCredentialType',
15+
description: 'An example credential type',
16+
schema_uri: 'http://example.com/schema/example',
17+
//this value could be generated on demand to make it easier when changing the values
18+
'schema_uri#Integrity':
19+
'sha256-48a61b283ded3b55e8d9a9b063327641dc4c53f76bd5daa96c23f232822167ae',
20+
};
21+
1222
const restHandlers = [
1323
http.get('http://example.com/schema/example', () => {
1424
const res = {
@@ -42,15 +52,7 @@ const restHandlers = [
4252
return HttpResponse.json(res);
4353
}),
4454
http.get('http://example.com/example', () => {
45-
const res: TypeMetadataFormat = {
46-
vct: 'http://example.com/example',
47-
name: 'ExampleCredentialType',
48-
description: 'An example credential type',
49-
schema_uri: 'http://example.com/schema/example',
50-
//this value could be generated on demand to make it easier when changing the values
51-
'schema_uri#Integrity':
52-
'sha256-48a61b283ded3b55e8d9a9b063327641dc4c53f76bd5daa96c23f232822167ae',
53-
};
55+
const res: TypeMetadataFormat = exampleVctm;
5456
return HttpResponse.json(res);
5557
}),
5658
http.get('http://example.com/timeout', () => {
@@ -133,6 +135,26 @@ describe('App', () => {
133135
await sdjwt.verify(encodedSdjwt);
134136
});
135137

138+
test('VCT from JWT header Validation', async () => {
139+
const expectedPayload: SdJwtVcPayload = {
140+
iat,
141+
iss,
142+
vct,
143+
'vct#Integrity': vctIntegrity,
144+
...claims,
145+
};
146+
const header = {
147+
vctm: [Buffer.from(JSON.stringify(exampleVctm)).toString('base64url')],
148+
};
149+
const encodedSdjwt = await sdjwt.issue(
150+
expectedPayload,
151+
disclosureFrame as unknown as DisclosureFrame<SdJwtVcPayload>,
152+
{ header },
153+
);
154+
155+
await sdjwt.verify(encodedSdjwt);
156+
});
157+
136158
test('VCT Validation with timeout', async () => {
137159
const vct = 'http://example.com/timeout';
138160
const expectedPayload: SdJwtVcPayload = {

0 commit comments

Comments
 (0)