From b4feb3c3f5f017013396b49d475b4a4e3eb9e15a Mon Sep 17 00:00:00 2001 From: "Lukas.J.Han" Date: Fri, 19 Sep 2025 01:39:44 +0900 Subject: [PATCH 1/2] feat: add vcld Signed-off-by: Lukas.J.Han --- packages/vcld/README.md | 39 +++++ packages/vcld/package.json | 67 ++++++++ packages/vcld/src/index.ts | 51 ++++++ packages/vcld/src/present.ts | 26 +++ packages/vcld/src/sign.ts | 182 ++++++++++++++++++++ packages/vcld/src/test/index.spec.ts | 7 + packages/vcld/src/test/sign.spec.ts | 241 +++++++++++++++++++++++++++ packages/vcld/src/type.ts | 23 +++ packages/vcld/src/verify.ts | 91 ++++++++++ packages/vcld/tsconfig.json | 7 + packages/vcld/vitest.config.mts | 4 + pnpm-lock.yaml | 173 +++++++++++++++++++ 12 files changed, 911 insertions(+) create mode 100644 packages/vcld/README.md create mode 100644 packages/vcld/package.json create mode 100644 packages/vcld/src/index.ts create mode 100644 packages/vcld/src/present.ts create mode 100644 packages/vcld/src/sign.ts create mode 100644 packages/vcld/src/test/index.spec.ts create mode 100644 packages/vcld/src/test/sign.spec.ts create mode 100644 packages/vcld/src/type.ts create mode 100644 packages/vcld/src/verify.ts create mode 100644 packages/vcld/tsconfig.json create mode 100644 packages/vcld/vitest.config.mts diff --git a/packages/vcld/README.md b/packages/vcld/README.md new file mode 100644 index 00000000..e1dc9ece --- /dev/null +++ b/packages/vcld/README.md @@ -0,0 +1,39 @@ +![License](https://img.shields.io/github/license/openwallet-foundation-labs/sd-jwt-js.svg) +![NPM](https://img.shields.io/npm/v/%40sd-jwt%2Fvcld) +![Release](https://img.shields.io/github/v/release/openwallet-foundation-labs/sd-jwt-js) +![Stars](https://img.shields.io/github/stars/openwallet-foundation-labs/sd-jwt-js) + +# SD-JWT Implementation in JavaScript (TypeScript) + +## SD-JWT VCLD + +### About + +SD-JWT VCLD + +Check the detail description in our github [repo](https://github.com/openwallet-foundation-labs/sd-jwt-js). + +### Installation + +To install this project, run the following command: + +```bash +# using npm +npm install @sd-jwt/vcld + +# using yarn +yarn add @sd-jwt/vcld + +# using pnpm +pnpm install @sd-jwt/vcld +``` + +Ensure you have Node.js installed as a prerequisite. + +### Usage + +Check out more details in our [documentation](https://github.com/openwallet-foundation-labs/sd-jwt-js/tree/main/docs) or [examples](https://github.com/openwallet-foundation-labs/sd-jwt-js/tree/main/examples) + +### Dependencies + +None diff --git a/packages/vcld/package.json b/packages/vcld/package.json new file mode 100644 index 00000000..22b4b911 --- /dev/null +++ b/packages/vcld/package.json @@ -0,0 +1,67 @@ +{ + "name": "@sd-jwt/vcld", + "version": "0.15.0", + "description": "sd-jwt draft 7 implementation in typescript", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "rm -rf **/dist && tsup", + "lint": "biome lint ./src", + "test": "pnpm run test:node && pnpm run test:browser && pnpm run test:cov", + "test:node": "vitest run ./src/test/*.spec.ts", + "test:browser": "vitest run ./src/test/*.spec.ts --environment jsdom", + "test:cov": "vitest run --coverage" + }, + "keywords": [ + "sd-jwt", + "sdjwt", + "sd-jwt-vc" + ], + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation-labs/sd-jwt-js" + }, + "author": "Lukas.J.Han ", + "homepage": "https://github.com/openwallet-foundation-labs/sd-jwt-js/wiki", + "bugs": { + "url": "https://github.com/openwallet-foundation-labs/sd-jwt-js/issues" + }, + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "tsup": { + "entry": [ + "./src/index.ts" + ], + "sourceMap": true, + "splitting": false, + "clean": true, + "dts": true, + "format": [ + "cjs", + "esm" + ] + }, + "dependencies": { + "@sd-jwt/core": "workspace:*", + "@sd-jwt/crypto-nodejs": "workspace:*", + "@sd-jwt/decode": "workspace:*", + "@sd-jwt/hash": "workspace:*", + "@sd-jwt/sd-jwt-vc": "workspace:*", + "@sd-jwt/types": "workspace:*", + "@types/jsonld": "^1.5.15", + "jsonld": "^8.3.3" + }, + "gitHead": "ded40e4551bde7ae93083181bf26bd1b38bbfcfb" +} diff --git a/packages/vcld/src/index.ts b/packages/vcld/src/index.ts new file mode 100644 index 00000000..d8516236 --- /dev/null +++ b/packages/vcld/src/index.ts @@ -0,0 +1,51 @@ +/** + * B.3.7. SD-JWT VCLD + +SD-JWT VCLD (SD-JWT Verifiable Credentials with JSON-LD) extends the IETF SD-JWT VC [I-D.ietf-oauth-sd-jwt-vc] Credential format and allows to incorporate existing data models that use Linked Data, e.g., W3C VCDM [VC_DATA], while enabling a consistent and uncomplicated approach to selective disclosure. +Information contained in SD-JWT VCLD Credentials can be processed using a JSON-LD [JSON-LD] processor after the SD-JWT VC processing.When IETF SD-JWT VC is mentioned in this specification, SD-JWT VCLD defined in this section MAY be used. + +B.3.7.1. Format + +SD-JWT VCLD Credentials are valid SD-JWT VCs and all requirements from [I-D.ietf-oauth-sd-jwt-vc] apply. Additionally, the requirements listed in this section apply. +For compatibility with JWT processors, the following registered Claims from [RFC7519] and [I-D.ietf-oauth-sd-jwt-vc] MUST be used instead of any respective counterpart properties from W3C VCDM or elsewhere: + +- vct to represent the type of the Credential. +- exp and nbf to represent the validity period of SD-JWT VCLD (i.e., cryptographic signature). +- iss to represent the Credential Issuer. status to represent the information to obtain the status of the Credential. + +IETF SD-JWT VC is extended with the following claim: + +- ld: OPTIONAL. Contains a JSON-LD [JSON-LD] object in compact form, e.g., [VC_DATA]. + +B.3.7.2. Processing + +The following outlines a suggested non-normative set of processing steps for SD-JWT VCLD: + +B.3.7.2.1. Step 1: SD-JWT VC Processing + +- A receiver (holder or verifier) of an SD-JWT VCLD applies the processing rules outlined in Section 4 of [I-D.ietf-oauth-sd-jwt-vc], including verifying signatures, validity periods, status information, etc. +- If the vct value is associated with any SD-JWT VC Type Metadata, schema validation of the entire SD-JWT VCLD is performed, including the nested ld claim. +- Additionally, trust framework rules are applied, such as ensuring the Credential Issuer is authorized to issue SD-JWT VCLDs for the specified vct value. + +B.3.7.2.2. Step 2: Business Logic Processing + +- Once the SD-JWT VC is verified and trusted by the SD-JWT VC processor, and if the ld claim is present, the receiver extracts the JSON-LD object from the ld claim and uses this for the business logic object. + If the ld claim is not present, the entire SD-JWT VC is considered to represent the business logic object. +- The business logic object is then passed on for further use case-specific processing and validation. + The business logic assumes that all security-critical functions (e.g., signature verification, trusted issuer) have already been performed during the previous step. + Additional schema validation is applied if provided in the ld claim, e.g., to support SHACL schemas. Note that while a vct claim is required, SD-JWT VC type metadata resolution and related schema validation is optional in certain cases. + + */ + +import { Present } from './present'; +import { decode, Signer } from './sign'; +import { JWTVerifier } from './verify'; + +export * from './type'; + +export const VCld = { + Signer, + decode, + Present, + Verify: JWTVerifier, +}; diff --git a/packages/vcld/src/present.ts b/packages/vcld/src/present.ts new file mode 100644 index 00000000..2f8e7fc5 --- /dev/null +++ b/packages/vcld/src/present.ts @@ -0,0 +1,26 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; +import { type PresentationFrame } from '@sd-jwt/types'; + +export const Present = { + async present>( + credential: string, + presentationFrame?: PresentationFrame, + options?: Record, + ): Promise { + // Initialize the SD JWT instance with proper configuration + const sdJwtInstance = new SDJwtInstance({ + hashAlg: 'sha-256', + hasher: digest, + saltGenerator: generateSalt, + }); + + // Use the instance's present method for the core SD-JWT functionality + const presentedCredential = await sdJwtInstance.present( + credential, + presentationFrame, + ); + + return presentedCredential; + }, +}; diff --git a/packages/vcld/src/sign.ts b/packages/vcld/src/sign.ts new file mode 100644 index 00000000..fc5a252d --- /dev/null +++ b/packages/vcld/src/sign.ts @@ -0,0 +1,182 @@ +import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode'; +import { hasher } from '@sd-jwt/hash'; +import { JsonLdDocument } from 'jsonld'; +import { SDJwtInstance } from '@sd-jwt/core'; +import { createSign } from 'node:crypto'; +import type { DisclosureFrame } from '@sd-jwt/types'; +import { type KeyObject } from 'node:crypto'; +import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; +import { ALGORITHMS, type Alg } from './type'; + +export class Signer { + private doc: JsonLdDocument; + private signAlg: Alg; + // TODO: fix type + private disclosureFrame: DisclosureFrame | undefined; + private header: Record | undefined; + + private vct: string; + private iss: string | undefined; + private exp: number | undefined; + private nbf: number | undefined; + + constructor(doc: JsonLdDocument, vct: string) { + this.doc = doc; + this.signAlg = 'ES256'; + this.vct = vct; + } + + setSignAlg(signAlg: Alg) { + this.signAlg = signAlg; + return this; + } + + setDisclosureFrame(disclosureFrame: DisclosureFrame) { + this.disclosureFrame = disclosureFrame; + return this; + } + + setHeader(header: Record) { + this.header = header; + return this; + } + + setIss(iss: string) { + this.iss = iss; + return this; + } + + setExp(exp: number) { + this.exp = exp; + return this; + } + + setNbf(nbf: number) { + this.nbf = nbf; + return this; + } + + async sign(key: KeyObject) { + if (!this.iss) throw new Error('iss must be set when signing'); + if (!this.exp) throw new Error('exp must be set when signing'); + if (!this.nbf) throw new Error('nbf must be set when signing'); + if (!this.signAlg) throw new Error('alg must be set when signing'); + + const sdjwtInstance = new SDJwtInstance({ + hashAlg: 'sha-256', + signAlg: this.signAlg, + hasher: digest, + saltGenerator: generateSalt, + signer: (data: string) => { + return JWTSigner.sign(this.signAlg, data, key); + }, + }); + + const payload = { + vct: this.vct, + iss: this.iss, + exp: this.exp, + nbf: this.nbf, + ld: this.doc, + }; + const disclosureFrame = { ld: this.disclosureFrame }; + + // TODO: fix type + const compact = await sdjwtInstance.issue(payload, disclosureFrame as any, { + header: this.header, + }); + + return compact; + } +} + +export const decode = (compact: string) => { + const decodedSdJwt = decodeSdJwtSync(compact, hasher); + const claims = getClaimsSync( + decodedSdJwt.jwt.payload, + decodedSdJwt.disclosures, + hasher, + ) as Record; + + if ('ld' in claims) { + return { claims, ld: claims['ld'] }; + } + + return { claims }; +}; + +const JWTSigner = { + sign(alg: Alg, signingInput: string, privateKey: KeyObject) { + const signature = JWTSigner.createSignature(alg, signingInput, privateKey); + return signature; + }, + + createSignature(alg: Alg, signingInput: string, privateKey: KeyObject) { + switch (alg) { + case 'RS256': + case 'RS384': + case 'RS512': + case 'PS256': + case 'PS384': + case 'PS512': { + const option = ALGORITHMS[alg]; + return JWTSigner.createRSASignature(signingInput, privateKey, option); + } + case 'ES256': + case 'ES384': + case 'ES512': { + const option = ALGORITHMS[alg]; + return JWTSigner.createECDSASignature(signingInput, privateKey, option); + } + case 'EdDSA': { + const option = ALGORITHMS[alg]; + return JWTSigner.createEdDSASignature(signingInput, privateKey, option); + } + default: + } + throw new Error(`Unsupported algorithm: ${alg}`); + }, + + createRSASignature( + signingInput: string, + privateKey: KeyObject, + options: { hash: string; padding: number }, + ) { + const signer = createSign(options.hash); + signer.update(signingInput); + const signature = signer.sign({ + key: privateKey, + padding: options.padding, + }); + return signature.toString('base64url'); + }, + + createECDSASignature( + signingInput: string, + privateKey: KeyObject, + options: { hash: string; namedCurve: string }, + ) { + const signer = createSign(options.hash); + signer.update(signingInput); + + const signature = signer.sign({ + key: privateKey, + dsaEncoding: 'ieee-p1363', + }); + + return signature.toString('base64url'); + }, + + createEdDSASignature( + signingInput: string, + privateKey: KeyObject, + options: { curves: string[] }, + ) { + const signer = createSign(options.curves[0]); + signer.update(signingInput); + const signature = signer.sign({ + key: privateKey, + }); + return signature.toString('base64url'); + }, +}; diff --git a/packages/vcld/src/test/index.spec.ts b/packages/vcld/src/test/index.spec.ts new file mode 100644 index 00000000..aa8f1911 --- /dev/null +++ b/packages/vcld/src/test/index.spec.ts @@ -0,0 +1,7 @@ +import { describe, expect, test } from 'vitest'; + +describe('Test#1', () => { + test('Test#1', () => { + expect(1).toBe(1); + }); +}); diff --git a/packages/vcld/src/test/sign.spec.ts b/packages/vcld/src/test/sign.spec.ts new file mode 100644 index 00000000..6e43299c --- /dev/null +++ b/packages/vcld/src/test/sign.spec.ts @@ -0,0 +1,241 @@ +import { describe, expect, test, it, beforeAll } from 'vitest'; +import { Signer, decode } from '../sign'; // Adjusted path to '../sign' +import { generateKeyPairSync, type KeyObject } from 'node:crypto'; +import type { JsonLdDocument } from 'jsonld'; + +// Sample data (will be expanded) +const sampleDoc: JsonLdDocument = { + '@context': 'https://www.w3.org/2018/credentials/v1', + id: 'urn:uuid:12345678-1234-5678-1234-567812345678', + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'https://example.edu/issuers/14', + issuanceDate: '2023-01-01T00:00:00Z', + credentialSubject: { + id: 'did:example:ebfeb1f712ebc6f1c276e12ec21', + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science in Computer Science', + }, + }, +}; + +const vct = 'UniversityDegreeCredential'; + +let es256KeyPair: { publicKey: KeyObject; privateKey: KeyObject }; +let rs256KeyPair: { publicKey: KeyObject; privateKey: KeyObject }; +let ed25519KeyPair: { publicKey: KeyObject; privateKey: KeyObject }; + +describe('Signer and Decode Tests', () => { + beforeAll(() => { + es256KeyPair = generateKeyPairSync('ec', { + namedCurve: 'P-256', + }); + rs256KeyPair = generateKeyPairSync('rsa', { + modulusLength: 2048, + }); + ed25519KeyPair = generateKeyPairSync('ed25519'); + }); + + describe('Signer Class', () => { + it('should correctly initialize with constructor', () => { + const signer = new Signer(sampleDoc, vct); + expect(signer).toBeInstanceOf(Signer); + // @ts-expect-error access private member for test + expect(signer.doc).toEqual(sampleDoc); + // @ts-expect-error access private member for test + expect(signer.vct).toEqual(vct); + // @ts-expect-error access private member for test + expect(signer.signAlg).toEqual('ES256'); // Default + }); + + it('should set signAlg', () => { + const signer = new Signer(sampleDoc, vct); + signer.setSignAlg('RS256'); + // @ts-expect-error access private member for test + expect(signer.signAlg).toEqual('RS256'); + }); + + it('should set disclosureFrame', () => { + const signer = new Signer(sampleDoc, vct); + const frame = { credentialSubject: { _sd: ['degree'] } }; + signer.setDisclosureFrame(frame as any); + // @ts-expect-error access private member for test + expect(signer.disclosureFrame).toEqual(frame); + }); + + it('should set header', () => { + const signer = new Signer(sampleDoc, vct); + const header = { typ: 'vc+sd-jwt' }; + signer.setHeader(header); + // @ts-expect-error access private member for test + expect(signer.header).toEqual(header); + }); + + it('should set iss', () => { + const signer = new Signer(sampleDoc, vct); + const iss = 'https://example.com/issuer'; + signer.setIss(iss); + // @ts-expect-error access private member for test + expect(signer.iss).toEqual(iss); + }); + + it('should set exp', () => { + const signer = new Signer(sampleDoc, vct); + const exp = Math.floor(Date.now() / 1000) + 3600; + signer.setExp(exp); + // @ts-expect-error access private member for test + expect(signer.exp).toEqual(exp); + }); + + it('should set nbf', () => { + const signer = new Signer(sampleDoc, vct); + const nbf = Math.floor(Date.now() / 1000); + signer.setNbf(nbf); + // @ts-expect-error access private member for test + expect(signer.nbf).toEqual(nbf); + }); + + describe('sign method', () => { + const iss = 'https://example.com/issuer'; + const exp = Math.floor(Date.now() / 1000) + 3600; + const nbf = Math.floor(Date.now() / 1000); + + it('should throw error if iss is not set', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setExp(exp); + signer.setNbf(nbf); + await expect(signer.sign(es256KeyPair.privateKey)).rejects.toThrow( + 'iss must be set when signing', + ); + }); + + it('should throw error if exp is not set', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setIss(iss); + signer.setNbf(nbf); + await expect(signer.sign(es256KeyPair.privateKey)).rejects.toThrow( + 'exp must be set when signing', + ); + }); + + it('should throw error if nbf is not set', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setIss(iss); + signer.setExp(exp); + await expect(signer.sign(es256KeyPair.privateKey)).rejects.toThrow( + 'nbf must be set when signing', + ); + }); + + it('should sign successfully with ES256', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setIss(iss); + signer.setExp(exp); + signer.setNbf(nbf); + signer.setSignAlg('ES256'); + const compactJwt = await signer.sign(es256KeyPair.privateKey); + expect(compactJwt).toBeTypeOf('string'); + expect(compactJwt.split('.').length).toBeGreaterThanOrEqual(3); // JWT.Disclosures... + }); + + it('should sign successfully with RS256', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setIss(iss); + signer.setExp(exp); + signer.setNbf(nbf); + signer.setSignAlg('RS256'); + const compactJwt = await signer.sign(rs256KeyPair.privateKey); + expect(compactJwt).toBeTypeOf('string'); + expect(compactJwt.split('.').length).toBeGreaterThanOrEqual(3); + }); + + it('should sign successfully with PS256', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setIss(iss); + signer.setExp(exp); + signer.setNbf(nbf); + signer.setSignAlg('PS256'); + const compactJwt = await signer.sign(rs256KeyPair.privateKey); // RSA key can be used for PS256 + expect(compactJwt).toBeTypeOf('string'); + expect(compactJwt.split('.').length).toBeGreaterThanOrEqual(3); + }); + }); + }); + + describe('decode function', () => { + const iss = 'https://example.com/issuer'; + const exp = Math.floor(Date.now() / 1000) + 3600; + const nbf = Math.floor(Date.now() / 1000); + const disclosureFrame = { + credentialSubject: { _sd: ['degree'] }, + } as any; + + it('should decode a signed JWT (ES256) and verify claims', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setIss(iss); + signer.setExp(exp); + signer.setNbf(nbf); + signer.setSignAlg('ES256'); + signer.setDisclosureFrame(disclosureFrame); + + const compactJwt = await signer.sign(es256KeyPair.privateKey); + const { claims, ld } = decode(compactJwt); + + expect(claims.iss).toEqual(iss); + expect(claims.exp).toEqual(exp); + expect(claims.nbf).toEqual(nbf); + expect(claims.vct).toEqual(vct); + expect(ld).toBeDefined(); + // @ts-expect-error ld is checked + expect(ld.credentialSubject.degree).toBeDefined(); // Check selectively disclosed part + // @ts-expect-error ld is checked + expect(ld.id).toEqual(sampleDoc.id); + }); + + it('should decode a signed JWT (RS256) and verify claims', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setIss(iss); + signer.setExp(exp); + signer.setNbf(nbf); + signer.setSignAlg('RS256'); + signer.setDisclosureFrame(disclosureFrame); + + const compactJwt = await signer.sign(rs256KeyPair.privateKey); + const { claims, ld } = decode(compactJwt); + + expect(claims.iss).toEqual(iss); + expect(claims.exp).toEqual(exp); + expect(claims.nbf).toEqual(nbf); + expect(claims.vct).toEqual(vct); + expect(ld).toBeDefined(); + // @ts-expect-error ld is checked + expect(ld.credentialSubject.degree).toBeDefined(); + // @ts-expect-error ld is checked + expect(ld.id).toEqual(sampleDoc.id); + }); + + it('should decode a signed JWT without disclosures', async () => { + const signer = new Signer(sampleDoc, vct); + signer.setIss(iss); + signer.setExp(exp); + signer.setNbf(nbf); + signer.setSignAlg('ES256'); + // No disclosureFrame set + + const compactJwt = await signer.sign(es256KeyPair.privateKey); + const { claims, ld } = decode(compactJwt); + + expect(claims.iss).toEqual(iss); + expect(claims.exp).toEqual(exp); + expect(claims.nbf).toEqual(nbf); + expect(claims.vct).toEqual(vct); + expect(ld).toBeDefined(); + // @ts-expect-error ld is checked + expect(ld?.credentialSubject?.degree?.name).toEqual( + (sampleDoc?.credentialSubject as any)?.degree?.name, + ); // Entire degree object should be present + // @ts-expect-error ld is checked + expect(ld?.id).toEqual(sampleDoc?.id); + }); + }); +}); diff --git a/packages/vcld/src/type.ts b/packages/vcld/src/type.ts new file mode 100644 index 00000000..f0fdea42 --- /dev/null +++ b/packages/vcld/src/type.ts @@ -0,0 +1,23 @@ +import { constants } from 'node:crypto'; + +export const ALGORITHMS = { + // RSA + RS256: { hash: 'sha256', padding: constants.RSA_PKCS1_PADDING }, + RS384: { hash: 'sha384', padding: constants.RSA_PKCS1_PADDING }, + RS512: { hash: 'sha512', padding: constants.RSA_PKCS1_PADDING }, + + // RSA-PSS + PS256: { hash: 'sha256', padding: constants.RSA_PKCS1_PSS_PADDING }, + PS384: { hash: 'sha384', padding: constants.RSA_PKCS1_PSS_PADDING }, + PS512: { hash: 'sha512', padding: constants.RSA_PKCS1_PSS_PADDING }, + + // ECDSA + ES256: { hash: 'sha256', namedCurve: 'P-256' }, + ES384: { hash: 'sha384', namedCurve: 'P-384' }, + ES512: { hash: 'sha512', namedCurve: 'P-521' }, + + // EdDSA + EdDSA: { curves: ['ed25519', 'ed448'] }, +}; + +export type Alg = keyof typeof ALGORITHMS; diff --git a/packages/vcld/src/verify.ts b/packages/vcld/src/verify.ts new file mode 100644 index 00000000..cd65c25a --- /dev/null +++ b/packages/vcld/src/verify.ts @@ -0,0 +1,91 @@ +import { createVerify, X509Certificate } from 'node:crypto'; +import { SDJwtInstance } from '@sd-jwt/core'; +import { digest } from '@sd-jwt/crypto-nodejs'; + +export const JWTVerifier = { + async verify(credential: string, requiredClaimKeys?: string[]) { + const instance = new SDJwtInstance({ + hasher: digest, + verifier: JWTVerifier.verifier, + }); + + const verifiedData = await instance.verify(credential, requiredClaimKeys); + return verifiedData; + }, + + verifier(data: string, signatureB64: string): boolean { + try { + const [headerB64, payloadB64] = data.split('.'); + + const headerStr = Buffer.from(headerB64, 'base64url').toString('utf-8'); + const header = JSON.parse(headerStr); + + if (!header.x5c || !Array.isArray(header.x5c)) { + throw new Error('x5c certificate chain is missing in header'); + } + + const isValid = JWTVerifier.verifySig( + data, + signatureB64, + header.x5c, + header.alg, + ); + + return isValid; + } catch (error) { + return false; + } + }, + + getVerifyAlgorithm(jwtAlg: string): string { + const algorithmMap: Record = { + RS256: 'SHA256', + RS384: 'SHA384', + RS512: 'SHA512', + ES256: 'SHA256', + ES384: 'SHA384', + ES512: 'SHA512', + PS256: 'SHA256', + PS384: 'SHA384', + PS512: 'SHA512', + }; + + const algorithm = algorithmMap[jwtAlg]; + if (!algorithm) { + throw new Error(`Unsupported JWT algorithm: ${jwtAlg}`); + } + + return algorithm; + }, + + verifySig( + data: string, + sig: string, + x5c: string[], + algorithm: string, + ): boolean { + try { + if (!x5c || x5c.length === 0) { + console.error('x5c certificate chain is missing'); + return false; + } + + const certDer = Buffer.from(x5c[0], 'base64'); + const cert = new X509Certificate(new Uint8Array(certDer)); + const publicKey = cert.publicKey; + + const signatureBytes = Buffer.from(sig, 'base64url'); + const signatureUint8Array = new Uint8Array(signatureBytes); + + const cryptoAlgorithm = JWTVerifier.getVerifyAlgorithm(algorithm); + + const verifier = createVerify(cryptoAlgorithm); + verifier.update(data); + + return verifier.verify(publicKey, signatureUint8Array); + } catch (error) { + console.error('JWT verification error:', error); + return false; + } + }, +}; diff --git a/packages/vcld/tsconfig.json b/packages/vcld/tsconfig.json new file mode 100644 index 00000000..2a11ecdb --- /dev/null +++ b/packages/vcld/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + } +} diff --git a/packages/vcld/vitest.config.mts b/packages/vcld/vitest.config.mts new file mode 100644 index 00000000..5842dffb --- /dev/null +++ b/packages/vcld/vitest.config.mts @@ -0,0 +1,4 @@ +// vite.config.ts +import { allEnvs } from '../../vitest.shared'; + +export default allEnvs; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af0dd5b4..76c1f79d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -266,6 +266,33 @@ importers: specifier: workspace:* version: link:../node-crypto + packages/vcld: + dependencies: + '@sd-jwt/core': + specifier: workspace:* + version: link:../core + '@sd-jwt/crypto-nodejs': + specifier: workspace:* + version: link:../node-crypto + '@sd-jwt/decode': + specifier: workspace:* + version: link:../decode + '@sd-jwt/hash': + specifier: workspace:* + version: link:../hash + '@sd-jwt/sd-jwt-vc': + specifier: workspace:* + version: link:../sd-jwt-vc + '@sd-jwt/types': + specifier: workspace:* + version: link:../types + '@types/jsonld': + specifier: ^1.5.15 + version: 1.5.15 + jsonld: + specifier: ^8.3.3 + version: 8.3.3(web-streams-polyfill@3.3.3) + packages: '@ampproject/remapping@2.2.1': @@ -366,6 +393,10 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@digitalbazaar/http-client@3.4.1': + resolution: {integrity: sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==} + engines: {node: '>=14.0'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -654,6 +685,10 @@ packages: cpu: [x64] os: [win32] + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@hutson/parse-repository-url@3.0.2': resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} engines: {node: '>=6.9.0'} @@ -1200,6 +1235,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/jsonld@1.5.15': + resolution: {integrity: sha512-PlAFPZjL+AuGYmwlqwKEL0IMP8M8RexH0NIPGfCVWSQ041H2rR/8OlyZSD7KsCVoN8vCfWdtWDBxX8yBVP+xow==} + '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} @@ -1275,6 +1313,10 @@ packages: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} @@ -1478,6 +1520,9 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + canonicalize@1.0.8: + resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==} + chai@4.5.0: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} @@ -1670,6 +1715,10 @@ packages: resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} engines: {node: '>=8'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -1830,6 +1879,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -1869,6 +1922,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -1913,6 +1970,10 @@ packages: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -2385,6 +2446,10 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonld@8.3.3: + resolution: {integrity: sha512-9YcilrF+dLfg9NTEof/mJLMtbdX1RJ8dbWtJgE00cMOIohb1lIyJl710vFiTaiHTl6ZYODJuBd32xFvUhmv3kg==} + engines: {node: '>=14'} + jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} @@ -2393,6 +2458,20 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + ky-universal@0.11.0: + resolution: {integrity: sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==} + engines: {node: '>=14.16'} + peerDependencies: + ky: '>=0.31.4' + web-streams-polyfill: '>=3.2.1' + peerDependenciesMeta: + web-streams-polyfill: + optional: true + + ky@0.33.3: + resolution: {integrity: sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==} + engines: {node: '>=14.16'} + lerna@8.1.2: resolution: {integrity: sha512-RCyBAn3XsqqvHbz3TxLfD7ylqzCi1A2UJnFEZmhURgx589vM3qYWQa/uOMeEEf565q6cAdtmulITciX1wgkAtw==} engines: {node: '>=18.0.0'} @@ -2678,6 +2757,11 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -2687,6 +2771,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp@10.0.1: resolution: {integrity: sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==} engines: {node: ^16.14.0 || >=18.0.0} @@ -3084,6 +3172,10 @@ packages: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} + rdf-canonize@3.4.0: + resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==} + engines: {node: '>=12'} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -3237,6 +3329,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} @@ -3626,6 +3721,10 @@ packages: undici-types@6.18.2: resolution: {integrity: sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==} + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + unique-filename@3.0.0: resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3740,6 +3839,10 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -3963,6 +4066,14 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@3.3.3)': + dependencies: + ky: 0.33.3 + ky-universal: 0.11.0(ky@0.33.3)(web-streams-polyfill@3.3.3) + undici: 5.29.0 + transitivePeerDependencies: + - web-streams-polyfill + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -4107,6 +4218,8 @@ snapshots: '@esbuild/win32-x64@0.24.2': optional: true + '@fastify/busboy@2.1.1': {} + '@hutson/parse-repository-url@3.0.2': {} '@inquirer/confirm@3.1.22': @@ -4684,6 +4797,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/jsonld@1.5.15': {} + '@types/minimatch@3.0.5': {} '@types/minimist@1.2.5': {} @@ -4784,6 +4899,10 @@ snapshots: abbrev@2.0.0: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-walk@8.3.2: {} acorn-walk@8.3.4: @@ -4980,6 +5099,8 @@ snapshots: camelcase@5.3.1: {} + canonicalize@1.0.8: {} + chai@4.5.0: dependencies: assertion-error: 1.1.0 @@ -5186,6 +5307,8 @@ snapshots: dargs@7.0.0: {} + data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -5347,6 +5470,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} execa@5.0.0: @@ -5401,6 +5526,11 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -5442,6 +5572,10 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + fs-constants@1.0.0: {} fs-extra@11.2.0: @@ -5934,10 +6068,29 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonld@8.3.3(web-streams-polyfill@3.3.3): + dependencies: + '@digitalbazaar/http-client': 3.4.1(web-streams-polyfill@3.3.3) + canonicalize: 1.0.8 + lru-cache: 6.0.0 + rdf-canonize: 3.4.0 + transitivePeerDependencies: + - web-streams-polyfill + jsonparse@1.3.1: {} kind-of@6.0.3: {} + ky-universal@0.11.0(ky@0.33.3)(web-streams-polyfill@3.3.3): + dependencies: + abort-controller: 3.0.0 + ky: 0.33.3 + node-fetch: 3.3.2 + optionalDependencies: + web-streams-polyfill: 3.3.3 + + ky@0.33.3: {} + lerna@8.1.2(encoding@0.1.13): dependencies: '@lerna/create': 8.1.2(encoding@0.1.13)(typescript@5.4.5) @@ -6346,12 +6499,20 @@ snapshots: neo-async@2.6.2: {} + node-domexception@1.0.0: {} + node-fetch@2.6.7(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 optionalDependencies: encoding: 0.1.13 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp@10.0.1: dependencies: env-paths: 2.2.1 @@ -6809,6 +6970,10 @@ snapshots: quick-lru@4.0.1: {} + rdf-canonize@3.4.0: + dependencies: + setimmediate: 1.0.5 + react-is@18.3.1: {} read-cmd-shim@4.0.0: {} @@ -7002,6 +7167,8 @@ snapshots: set-blocking@2.0.0: {} + setimmediate@1.0.5: {} + shallow-clone@3.0.1: dependencies: kind-of: 6.0.3 @@ -7424,6 +7591,10 @@ snapshots: undici-types@6.18.2: {} + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + unique-filename@3.0.0: dependencies: unique-slug: 4.0.0 @@ -7534,6 +7705,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@4.0.2: {} From 85c45197561012d9455ff7ed746d721367c84d4c Mon Sep 17 00:00:00 2001 From: "Lukas.J.Han" Date: Sun, 21 Sep 2025 23:47:11 +0900 Subject: [PATCH 2/2] ci: fix Signed-off-by: Lukas.J.Han --- packages/vcld/src/present.ts | 2 +- packages/vcld/src/sign.ts | 16 +++++++++------- packages/vcld/src/test/sign.spec.ts | 7 ++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/vcld/src/present.ts b/packages/vcld/src/present.ts index 2f8e7fc5..784cb07a 100644 --- a/packages/vcld/src/present.ts +++ b/packages/vcld/src/present.ts @@ -1,6 +1,6 @@ import { SDJwtInstance } from '@sd-jwt/core'; import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; -import { type PresentationFrame } from '@sd-jwt/types'; +import type { PresentationFrame } from '@sd-jwt/types'; export const Present = { async present>( diff --git a/packages/vcld/src/sign.ts b/packages/vcld/src/sign.ts index fc5a252d..407ce8c9 100644 --- a/packages/vcld/src/sign.ts +++ b/packages/vcld/src/sign.ts @@ -1,17 +1,17 @@ import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode'; import { hasher } from '@sd-jwt/hash'; -import { JsonLdDocument } from 'jsonld'; +import type { JsonLdDocument } from 'jsonld'; import { SDJwtInstance } from '@sd-jwt/core'; import { createSign } from 'node:crypto'; import type { DisclosureFrame } from '@sd-jwt/types'; -import { type KeyObject } from 'node:crypto'; +import type { KeyObject } from 'node:crypto'; import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; import { ALGORITHMS, type Alg } from './type'; export class Signer { private doc: JsonLdDocument; private signAlg: Alg; - // TODO: fix type + // biome-ignore lint/suspicious/noExplicitAny: use any for disclosureFrame private disclosureFrame: DisclosureFrame | undefined; private header: Record | undefined; @@ -31,6 +31,7 @@ export class Signer { return this; } + // biome-ignore lint/suspicious/noExplicitAny: use any for disclosureFrame setDisclosureFrame(disclosureFrame: DisclosureFrame) { this.disclosureFrame = disclosureFrame; return this; @@ -79,10 +80,11 @@ export class Signer { nbf: this.nbf, ld: this.doc, }; - const disclosureFrame = { ld: this.disclosureFrame }; + const disclosureFrame = { ld: this.disclosureFrame } as DisclosureFrame< + typeof payload + >; - // TODO: fix type - const compact = await sdjwtInstance.issue(payload, disclosureFrame as any, { + const compact = await sdjwtInstance.issue(payload, disclosureFrame, { header: this.header, }); @@ -99,7 +101,7 @@ export const decode = (compact: string) => { ) as Record; if ('ld' in claims) { - return { claims, ld: claims['ld'] }; + return { claims, ld: claims.ld }; } return { claims }; diff --git a/packages/vcld/src/test/sign.spec.ts b/packages/vcld/src/test/sign.spec.ts index 6e43299c..4d24c424 100644 --- a/packages/vcld/src/test/sign.spec.ts +++ b/packages/vcld/src/test/sign.spec.ts @@ -58,7 +58,7 @@ describe('Signer and Decode Tests', () => { it('should set disclosureFrame', () => { const signer = new Signer(sampleDoc, vct); const frame = { credentialSubject: { _sd: ['degree'] } }; - signer.setDisclosureFrame(frame as any); + signer.setDisclosureFrame(frame); // @ts-expect-error access private member for test expect(signer.disclosureFrame).toEqual(frame); }); @@ -168,7 +168,7 @@ describe('Signer and Decode Tests', () => { const nbf = Math.floor(Date.now() / 1000); const disclosureFrame = { credentialSubject: { _sd: ['degree'] }, - } as any; + }; it('should decode a signed JWT (ES256) and verify claims', async () => { const signer = new Signer(sampleDoc, vct); @@ -232,7 +232,8 @@ describe('Signer and Decode Tests', () => { expect(ld).toBeDefined(); // @ts-expect-error ld is checked expect(ld?.credentialSubject?.degree?.name).toEqual( - (sampleDoc?.credentialSubject as any)?.degree?.name, + // @ts-expect-error + sampleDoc?.credentialSubject?.degree?.name, ); // Entire degree object should be present // @ts-expect-error ld is checked expect(ld?.id).toEqual(sampleDoc?.id);