Skip to content

Commit 419c7c0

Browse files
committed
feat: add sd-jwt with jades support package
Signed-off-by: Lukas.J Han <[email protected]>
1 parent d2f2cb5 commit 419c7c0

23 files changed

+1771
-0
lines changed

packages/jades/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5+
6+
## [0.1.2](https://github.com/lukasjhan/sd-jwt-vc-dm-owf/compare/v0.1.1...v0.1.2) (2025-02-09)
7+
8+
**Note:** Version bump only for package sd-jwt-jades
9+
10+
11+
12+
13+
14+
## 0.1.1 (2025-02-09)
15+
16+
**Note:** Version bump only for package jades

packages/jades/README.md

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# SD JWT VCDM Typescript
2+
3+
> ⚠️ **Platform Support**: This package currently supports Node.js environments only.
4+
5+
Typescript implementation of SD JWT VCDM profile.
6+
7+
A library that integrates SD-JWT with W3C Verifiable Credentials Data Model and implements JAdES digital signature standards.
8+
9+
## Features
10+
11+
### SD-JWT VCDM Data Model Profile
12+
13+
This library provides interoperability between SD-JWT (Selective Disclosure JWT) and W3C Verifiable Credentials Data Model:
14+
15+
- Issue Verifiable Digital Credentials in SD-JWT VC format while maintaining W3C VCDM compliance
16+
- Support for Selective Disclosure capabilities
17+
- Seamless integration with standard VC verification processes
18+
19+
### JAdES Digital Signature Integration
20+
21+
Implements JAdES (JSON Advanced Electronic Signatures) standard for SD-JWT with support for the following signature profiles:
22+
23+
- **B-B (Basic - Baseline)**: Basic signature format
24+
- **B-T (Basic with Time)**: Signatures with timestamp
25+
- **B-LT (Basic Long-Term)**: Signatures with validation data for long-term preservation
26+
- **B-LTA (Basic Long-Term with Archive timestamps)**: Long-term preservation with periodic timestamp renewal
27+
28+
## Installation
29+
30+
```bash
31+
pnpm add sd-jwt-vcdm
32+
```
33+
34+
## Usage
35+
36+
### B-B
37+
38+
```typescript
39+
import { JAdES, parseCerts, createKidFromCert } from 'sd-jwt-jades';
40+
import * as fs from 'fs';
41+
import { createPrivateKey } from 'node:crypto';
42+
43+
(async () => {
44+
const jades = new JAdES.Sign({ data: 'data 1', target: 'data 2' });
45+
46+
const certPem = fs.readFileSync('./fixtures/certificate.crt', 'utf-8');
47+
const certs = parseCerts(certPem);
48+
const kid = createKidFromCert(certs[0]);
49+
50+
const keyPem = fs.readFileSync('./fixtures/private.pem', 'utf-8');
51+
const privateKey = createPrivateKey(keyPem);
52+
53+
await jades
54+
.setProtectedHeader({
55+
alg: 'RS256',
56+
typ: 'jades',
57+
})
58+
.setX5c(certs)
59+
.setDisclosureFrame({
60+
_sd: ['data'],
61+
})
62+
.setSignedAt()
63+
.sign(privateKey, kid);
64+
65+
const serialized = jades.toJSON();
66+
console.log(serialized);
67+
})();
68+
```
69+
70+
### B-T
71+
72+
```typescript
73+
import { JAdES, parseCerts, createKidFromCert } from 'sd-jwt-jades';
74+
import * as fs from 'fs';
75+
import { createPrivateKey } from 'node:crypto';
76+
77+
(async () => {
78+
const jades = new JAdES.Sign({ data: 'data 1', target: 'data 2' });
79+
80+
const certPem = fs.readFileSync('./fixtures/certificate.crt', 'utf-8');
81+
const certs = parseCerts(certPem);
82+
const kid = createKidFromCert(certs[0]);
83+
84+
const keyPem = fs.readFileSync('./fixtures/private.pem', 'utf-8');
85+
const privateKey = createPrivateKey(keyPem);
86+
87+
await jades
88+
.setProtectedHeader({
89+
alg: 'RS256',
90+
typ: 'jades',
91+
})
92+
.setX5c(certs)
93+
.setDisclosureFrame({
94+
_sd: ['data'],
95+
})
96+
.setSignedAt()
97+
.setUnprotectedHeader({
98+
etsiU: [
99+
{
100+
sigTst: {
101+
tstTokens: [
102+
{
103+
val: 'Base64-encoded RFC 3161 Timestamp Token',
104+
},
105+
],
106+
},
107+
},
108+
],
109+
})
110+
.sign(privateKey, kid);
111+
112+
const serialized = jades.toJSON();
113+
console.log(serialized);
114+
})();
115+
```
116+
117+
### B-LT
118+
119+
```typescript
120+
import { JAdES, parseCerts, createKidFromCert } from 'sd-jwt-jades';
121+
import * as fs from 'fs';
122+
import { createPrivateKey } from 'node:crypto';
123+
124+
(async () => {
125+
const jades = new JAdES.Sign({ data: 'data 1', target: 'data 2' });
126+
127+
const certPem = fs.readFileSync('./fixtures/certificate.crt', 'utf-8');
128+
const certs = parseCerts(certPem);
129+
const kid = createKidFromCert(certs[0]);
130+
131+
const keyPem = fs.readFileSync('./fixtures/private.pem', 'utf-8');
132+
const privateKey = createPrivateKey(keyPem);
133+
134+
await jades
135+
.setProtectedHeader({
136+
alg: 'RS256',
137+
typ: 'jades',
138+
})
139+
.setX5c(certs)
140+
.setDisclosureFrame({
141+
_sd: ['data'],
142+
})
143+
.setSignedAt()
144+
.setUnprotectedHeader({
145+
etsiU: [
146+
{
147+
sigTst: {
148+
tstTokens: [
149+
{
150+
val: 'Base64-encoded RFC 3161 Timestamp Token',
151+
},
152+
],
153+
},
154+
},
155+
{
156+
xVals: [
157+
{ x509Cert: 'Base64-encoded Trust Anchor' },
158+
{ x509Cert: 'Base64-encoded CA Certificate' },
159+
],
160+
},
161+
{
162+
rVals: {
163+
crlVals: ['Base64-encoded CRL'],
164+
ocspVals: ['Base64-encoded OCSP Response'],
165+
},
166+
},
167+
],
168+
})
169+
.sign(privateKey, kid);
170+
171+
const serialized = jades.toJSON();
172+
console.log(serialized);
173+
})();
174+
```
175+
176+
### B-LTA
177+
178+
```typescript
179+
import { JAdES, parseCerts, createKidFromCert } from 'sd-jwt-jades';
180+
import * as fs from 'fs';
181+
import { createPrivateKey } from 'node:crypto';
182+
183+
(async () => {
184+
const jades = new JAdES.Sign({ data: 'data 1', target: 'data 2' });
185+
186+
const certPem = fs.readFileSync('./fixtures/certificate.crt', 'utf-8');
187+
const certs = parseCerts(certPem);
188+
const kid = createKidFromCert(certs[0]);
189+
190+
const keyPem = fs.readFileSync('./fixtures/private.pem', 'utf-8');
191+
const privateKey = createPrivateKey(keyPem);
192+
193+
await jades
194+
.setProtectedHeader({
195+
alg: 'RS256',
196+
typ: 'jades',
197+
})
198+
.setX5c(certs)
199+
.setDisclosureFrame({
200+
_sd: ['data'],
201+
})
202+
.setSignedAt()
203+
.sign(privateKey, kid);
204+
205+
const serialized = jades.toJSON();
206+
console.log(serialized);
207+
})();
208+
```
209+
210+
## License
211+
212+
Apache License 2.0
213+
214+
## References
215+
216+
- [SD-JWT VC Data Model](https://github.com/danielfett/sd-jwt-vc-dm)
217+
- [OpenID4VC HAIP Profile](https://github.com/openid/oid4vc-haip/pull/147/files#diff-762ef65fd82909517226ac1bb7e8855792bb57021abc1637c15b8557154dbbf1)
218+
- [ETSI TS 119 182-1 - JAdES Baseline Signatures](https://www.etsi.org/deliver/etsi_ts/119100_119199/11918201/01.02.01_60/ts_11918201v010201p.pdf)

packages/jades/package.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "sd-jwt-jades",
3+
"version": "0.1.2",
4+
"description": "JADES implementation in typescript",
5+
"main": "dist/index.js",
6+
"module": "dist/index.mjs",
7+
"types": "dist/index.d.ts",
8+
"exports": {
9+
".": {
10+
"import": "./dist/index.mjs",
11+
"require": "./dist/index.js"
12+
}
13+
},
14+
"scripts": {
15+
"build": "rm -rf **/dist && tsup",
16+
"lint": "biome lint ./src",
17+
"test": "pnpm run test:node && pnpm run test:browser && pnpm run test:cov",
18+
"test:node": "vitest run ./src/test/*.spec.ts",
19+
"test:browser": "vitest run ./src/test/*.spec.ts --environment jsdom",
20+
"test:cov": "vitest run --coverage"
21+
},
22+
"keywords": [
23+
"jades",
24+
"sd-jwt",
25+
"jwt"
26+
],
27+
"engines": {
28+
"node": ">=16"
29+
},
30+
"author": "Lukas.J.Han <[email protected]>",
31+
"license": "Apache-2.0",
32+
"publishConfig": {
33+
"access": "public"
34+
},
35+
"tsup": {
36+
"entry": [
37+
"./src/index.ts"
38+
],
39+
"sourceMap": true,
40+
"splitting": false,
41+
"clean": true,
42+
"dts": true,
43+
"format": [
44+
"cjs",
45+
"esm"
46+
]
47+
},
48+
"dependencies": {
49+
"@sd-jwt/core": "workspace:*",
50+
"@sd-jwt/crypto-nodejs": "workspace:*",
51+
"@sd-jwt/types": "workspace:*",
52+
"@sd-jwt/utils": "workspace:*",
53+
"asn1js": "^3.0.5"
54+
}
55+
}

packages/jades/src/constant.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { constants } from 'crypto';
2+
3+
export const ALGORITHMS = {
4+
// RSA
5+
RS256: { hash: 'sha256', padding: constants.RSA_PKCS1_PADDING },
6+
RS384: { hash: 'sha384', padding: constants.RSA_PKCS1_PADDING },
7+
RS512: { hash: 'sha512', padding: constants.RSA_PKCS1_PADDING },
8+
9+
// RSA-PSS
10+
PS256: { hash: 'sha256', padding: constants.RSA_PKCS1_PSS_PADDING },
11+
PS384: { hash: 'sha384', padding: constants.RSA_PKCS1_PSS_PADDING },
12+
PS512: { hash: 'sha512', padding: constants.RSA_PKCS1_PSS_PADDING },
13+
14+
// ECDSA
15+
ES256: { hash: 'sha256', namedCurve: 'P-256' },
16+
ES384: { hash: 'sha384', namedCurve: 'P-384' },
17+
ES512: { hash: 'sha512', namedCurve: 'P-521' },
18+
19+
// EdDSA
20+
EdDSA: { curves: ['ed25519', 'ed448'] },
21+
};
22+
23+
export enum CommitmentOIDs {
24+
proofOfOrigin = '1.2.840.113549.1.9.16.6.1',
25+
proofOfReceipt = '1.2.840.113549.1.9.16.6.2',
26+
proofOfDelivery = '1.2.840.113549.1.9.16.6.3',
27+
proofOfSender = '1.2.840.113549.1.9.16.6.4',
28+
proofOfApproval = '1.2.840.113549.1.9.16.6.5',
29+
proofOfCreation = '1.2.840.113549.1.9.16.6.6',
30+
}

packages/jades/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Sign } from './sign';
2+
export * from './type';
3+
export * from './constant';
4+
export * from './utils';
5+
import { Present } from './present';
6+
import { JWTVerifier } from './verify';
7+
8+
export const JAdES = {
9+
Sign,
10+
Present,
11+
Verify: JWTVerifier,
12+
};

packages/jades/src/present.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { SDJwtGeneralJSONInstance } from '@sd-jwt/core';
2+
import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
3+
import { PresentationFrame } from '@sd-jwt/types';
4+
import { GeneralJWS } from './type';
5+
import { getGeneralJSONFromJWSToken } from './utils';
6+
7+
export class Present {
8+
public static async present<T extends Record<string, unknown>>(
9+
credential: GeneralJWS | string,
10+
presentationFrame?: PresentationFrame<T>,
11+
options?: Record<string, unknown>,
12+
): Promise<GeneralJWS> {
13+
// Initialize the SD JWT instance with proper configuration
14+
const sdJwtInstance = new SDJwtGeneralJSONInstance({
15+
hashAlg: 'sha-256',
16+
hasher: digest,
17+
saltGenerator: generateSalt,
18+
});
19+
20+
// Convert string to GeneralJSON if needed
21+
const generalJsonCredential = getGeneralJSONFromJWSToken(credential);
22+
23+
// If there are no disclosures, return the credential as is
24+
// This prevents errors from the core library when handling credentials without SD claims
25+
if (
26+
!generalJsonCredential.disclosures ||
27+
generalJsonCredential.disclosures.length === 0
28+
) {
29+
console.log(
30+
'Credential has no selective disclosure claims, returning as is',
31+
);
32+
return generalJsonCredential.toJson();
33+
}
34+
35+
// Use the instance's present method for the core SD-JWT functionality
36+
const presentedCredential = await sdJwtInstance.present(
37+
generalJsonCredential,
38+
presentationFrame,
39+
);
40+
41+
return presentedCredential.toJson();
42+
}
43+
}

0 commit comments

Comments
 (0)