Skip to content

Commit d062ecf

Browse files
authored
feat: Add support for vctm header (#32)
* feat: Add support for vctm header * removed deprecated createVCSDJWT method
1 parent 46f0bb8 commit d062ecf

4 files changed

Lines changed: 87 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@ and this project (loosely) adheres to [Semantic Versioning](https://semver.org/s
77

88
## 2.0.0 - 2025-XX-XX
99

10+
### Added
11+
12+
- Support embedding Type Metadata documents in the JWS unprotected header via the `vctm` parameter, as per [SD-JWT VC Spec Section 6.3.5](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-05.html#section-6.3.5).
13+
1014
### Changed
1115

1216
- The `typ` header for issued SD-JWT VCs is now `dc+sd-jwt` as per [draft-08](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-08.html#section-3.2.1)
1317
- The `Verifier` will accept both `vc+sd-jwt` and `dc+sd-jwt`.
1418

19+
### Removed
20+
21+
- Removed deprecated `createVCSDJWT` method from the `Issuer` class. Use `createSignedVCSDJWT` instead.
22+
1523
## 1.3.0 - 2024-10-07
1624

1725
### Security

src/issuer.spec.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
import { DisclosureFrame, base64decode, decodeDisclosures, decodeJWT } from '@meeco/sd-jwt';
12
import { generateKeyPair } from 'jose';
2-
3-
import { DisclosureFrame, decodeDisclosures, decodeJWT } from '@meeco/sd-jwt';
43
import { Issuer } from './issuer';
54
import { hasherCallbackFn, signerCallbackFn } from './test-utils/helpers';
65
import {
@@ -70,7 +69,13 @@ describe('Issuer', () => {
7069
],
7170
};
7271

73-
const VCSDJwt = await issuer.createVCSDJWT(vcClaims, payload, sdVCClaimsDisclosureFrame, undefined, sdVCHeader);
72+
const VCSDJwt = await issuer.createSignedVCSDJWT({
73+
vcClaims,
74+
sdJWTPayload: payload,
75+
sdVCClaimsDisclosureFrame,
76+
saltGenerator: undefined,
77+
sdJWTHeader: sdVCHeader,
78+
});
7479

7580
expect(VCSDJwt).toBeDefined();
7681
expect(typeof VCSDJwt).toBe('string');
@@ -117,6 +122,45 @@ describe('Issuer', () => {
117122
expect(jwtHeader.typ).toEqual(ValidTypValues.DCSDJWT);
118123
});
119124

125+
it('should embed type metadata in vctm header when provided', async () => {
126+
const payload: CreateSDJWTPayload = {
127+
iat: Math.floor(Date.now() / 1000),
128+
cnf: { jwk: { kty: 'EC', crv: 'P-256', x: 'x', y: 'y' } },
129+
iss: 'https://valid.issuer.url',
130+
vct: 'test_vct_for_vctm',
131+
};
132+
const vcClaims: VCClaims = { data: 'some_claim_data' };
133+
const typeMetadataDoc1 = { vct: 'test_vct_for_vctm', name: 'Test Credential Type 1' };
134+
const typeMetadataDoc2String = JSON.stringify({ vct: 'test_vct_for_vctm_extended', name: 'Extended Type' });
135+
136+
const VCSDJwtWithVctm = await issuer.createSignedVCSDJWT({
137+
vcClaims,
138+
sdJWTPayload: payload,
139+
typeMetadataGlueDocuments: [typeMetadataDoc1, typeMetadataDoc2String],
140+
});
141+
142+
const { header: headerWithVctm } = decodeJWT(VCSDJwtWithVctm.split(SD_JWT_FORMAT_SEPARATOR).shift() || '');
143+
expect(headerWithVctm.vctm).toBeDefined();
144+
expect(Array.isArray(headerWithVctm.vctm)).toBe(true);
145+
146+
expect((headerWithVctm.vctm as any[]).length).toBe(2);
147+
148+
const decodedDoc1 = JSON.parse(base64decode((headerWithVctm.vctm as any[])[0]));
149+
150+
const decodedDoc2 = JSON.parse(base64decode((headerWithVctm.vctm as any[])[1]));
151+
152+
expect(decodedDoc1).toEqual(typeMetadataDoc1);
153+
expect(decodedDoc2).toEqual(JSON.parse(typeMetadataDoc2String));
154+
155+
// Test without vctm
156+
const VCSDJwtWithoutVctm = await issuer.createSignedVCSDJWT({
157+
vcClaims,
158+
sdJWTPayload: payload,
159+
});
160+
const { header: headerWithoutVctm } = decodeJWT(VCSDJwtWithoutVctm.split(SD_JWT_FORMAT_SEPARATOR).shift() || '');
161+
expect(headerWithoutVctm.vctm).toBeUndefined();
162+
});
163+
120164
describe('Issuer', () => {
121165
describe('constructor', () => {
122166
it('should throw an error if signer callback is not provided', () => {

src/issuer.ts

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
import { DisclosureFrame, JWTHeaderParameters, SDJWTPayload, SaltGenerator, issueSDJWT } from '@meeco/sd-jwt';
1+
import { DisclosureFrame, JWTHeaderParameters, SDJWTPayload, base64encode, issueSDJWT } from '@meeco/sd-jwt';
22
import { SDJWTVCError } from './errors.js';
3-
import {
4-
CreateSDJWTPayload,
5-
CreateSignedJWTOpts,
6-
HasherConfig,
7-
JWT,
8-
ReservedJWTClaimKeys,
9-
SignerConfig,
10-
VCClaims,
11-
} from './types.js';
3+
import { CreateSignedJWTOpts, HasherConfig, JWT, ReservedJWTClaimKeys, SignerConfig, VCClaims } from './types.js';
124
import { ValidTypValues, isValidUrl } from './util.js';
135

146
export class Issuer {
@@ -42,58 +34,48 @@ export class Issuer {
4234
return this.hasher;
4335
}
4436

45-
/**
46-
* Creates a VC SD-JWT.
47-
* @deprecated This method will be removed in the next version. Use `createSignedVCSDJWT` instead.
48-
* @param claims The VC claims.
49-
* @param sdJWTPayload The SD-JWT payload.
50-
* @param sdVCClaimsDisclosureFrame The SD-VC claims disclosure frame.
51-
* @param saltGenerator The salt generator.
52-
* @param sdJWTHeader additional header parameters
53-
* @throws An error if the VC SD-JWT cannot be created.
54-
* @returns The VC SD-JWT.
55-
*/
56-
async createVCSDJWT(
57-
vcClaims: VCClaims,
58-
sdJWTPayload: CreateSDJWTPayload,
59-
sdVCClaimsDisclosureFrame: DisclosureFrame = {},
60-
saltGenerator?: SaltGenerator,
61-
sdJWTHeader?: Omit<JWTHeaderParameters, 'typ' | 'alg'>,
62-
): Promise<JWT> {
63-
return this.createSignedVCSDJWT({ vcClaims, sdJWTPayload, sdVCClaimsDisclosureFrame, saltGenerator, sdJWTHeader });
64-
}
65-
6637
/**
6738
* Creates a signed SD-JWT VC.
6839
*/
6940
async createSignedVCSDJWT(opts: CreateSignedJWTOpts): Promise<JWT> {
70-
const { vcClaims, sdJWTPayload, sdVCClaimsDisclosureFrame = {}, saltGenerator, sdJWTHeader } = opts;
41+
const {
42+
vcClaims,
43+
sdJWTPayload,
44+
sdVCClaimsDisclosureFrame = {},
45+
saltGenerator,
46+
sdJWTHeader,
47+
typeMetadataGlueDocuments,
48+
} = opts;
7149
if (!vcClaims) throw new SDJWTVCError('vcClaims is required');
7250
if (!sdJWTPayload) throw new SDJWTVCError('sdJWTPayload is required');
7351

7452
this.validateVCClaims(vcClaims as VCClaims);
7553
this.validateSDJWTPayload(sdJWTPayload);
7654
this.validateSDVCClaimsDisclosureFrame(sdVCClaimsDisclosureFrame);
7755

56+
const header: JWTHeaderParameters & { vctm?: string[] } = {
57+
...sdJWTHeader,
58+
typ: Issuer.SD_JWT_TYP,
59+
alg: this.signer.alg,
60+
};
61+
62+
if (typeMetadataGlueDocuments && typeMetadataGlueDocuments.length > 0) {
63+
header.vctm = typeMetadataGlueDocuments.map((doc) => {
64+
const docString = typeof doc === 'string' ? doc : JSON.stringify(doc);
65+
return base64encode(docString);
66+
});
67+
}
68+
7869
try {
79-
const jwt = await issueSDJWT(
80-
{
81-
...sdJWTHeader,
82-
typ: Issuer.SD_JWT_TYP,
83-
alg: this.signer.alg,
84-
},
85-
{ ...sdJWTPayload, ...vcClaims },
86-
sdVCClaimsDisclosureFrame,
87-
{
88-
signer: this.signer.callback,
89-
hash: {
90-
alg: this.hasher.alg,
91-
callback: this.hasher.callback,
92-
},
93-
cnf: sdJWTPayload?.cnf,
94-
generateSalt: saltGenerator,
70+
const jwt = await issueSDJWT(header, { ...sdJWTPayload, ...vcClaims }, sdVCClaimsDisclosureFrame, {
71+
signer: this.signer.callback,
72+
hash: {
73+
alg: this.hasher.alg,
74+
callback: this.hasher.callback,
9575
},
96-
);
76+
cnf: sdJWTPayload?.cnf,
77+
generateSalt: saltGenerator,
78+
});
9779

9880
return jwt;
9981
} catch (error: any) {

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface CreateSignedJWTOpts {
3131
sdVCClaimsDisclosureFrame?: DisclosureFrame;
3232
saltGenerator?: SaltGenerator;
3333
sdJWTHeader?: Omit<JWTHeaderParameters, 'typ' | 'alg'>;
34+
typeMetadataGlueDocuments?: Array<Record<string, any> | string>;
3435
}
3536

3637
export interface PresentSDJWTPayload extends JWTPayload {

0 commit comments

Comments
 (0)