diff --git a/examples/sd-jwt-example/README.md b/examples/sd-jwt-example/README.md new file mode 100644 index 0000000..bb9fb3c --- /dev/null +++ b/examples/sd-jwt-example/README.md @@ -0,0 +1,36 @@ +# SD JWT Core Examples + +This directory contains example of basic usage(issue, validate, present, verify) of SD JWT + +## Run the example + +```bash +pnpm run {example_file_name} + +# example +pnpm run all +``` + +### Example lists + +- basic: Example of basic usage(issue, validate, present, verify) of SD JWT +- all: Example of issue, present and verify the comprehensive data. +- custom: Example of using custom hasher and salt generator for SD JWT +- custom_header: Example of using custom header for SD JWT +- sdjwtobject: Example of using SD JWT Object +- decoy: Example of adding decoy digest in SD JWT +- kb: key binding example in SD JWT +- decode: Decoding example of a SD JWT sample + +### Variables In Examples + +- claims: the user's information +- disclosureFrame: specify which claims should be disclosed +- credential: Issued Encoded SD JWT. +- validated: result of SD JWT validation +- presentationFrame: specify which claims should be presented +- presentation: Presented Encoded SD JWT. +- requiredClaims: specify which claims should be verified +- verified: result of verification +- sdJwtToken: SD JWT Token Object +- SDJwtInstance: SD JWT Instance diff --git a/examples/sd-jwt-example/all.ts b/examples/sd-jwt-example/all.ts new file mode 100644 index 0000000..fec051e --- /dev/null +++ b/examples/sd-jwt-example/all.ts @@ -0,0 +1,105 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { DisclosureFrame } from '@sd-jwt/types'; +import { createSignerVerifier, digest, generateSalt } from './utils'; + +(async () => { + const { signer, verifier } = await createSignerVerifier(); + + // Create SDJwt instance for use + const sdjwt = new SDJwtInstance({ + signer, + verifier, + signAlg: 'EdDSA', + hasher: digest, + hashAlg: 'SHA-256', + saltGenerator: generateSalt, + }); + + // Issuer Define the claims object with the user's information + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + data: { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + list: [{ r: '1' }, 'b', 'c'], + }, + data2: { + hi: 'bye', + }, + }; + + // Issuer Define the disclosure frame to specify which claims can be disclosed + const disclosureFrame: DisclosureFrame = { + _sd: ['firstname', 'id', 'data2'], + data: { + _sd: ['list'], + _sd_decoy: 2, + list: { + _sd: [0, 2], + _sd_decoy: 1, + 0: { + _sd: ['r'], + }, + }, + }, + data2: { + _sd: ['hi'], + }, + }; + + // Issue a signed JWT credential with the specified claims and disclosures + // Return a Encoded SD JWT. Issuer send the credential to the holder + const credential = await sdjwt.issue(claims, disclosureFrame); + console.log('encodedJwt:', credential); + + // Holder Receive the credential from the issuer and validate it + // Return a boolean result + const validated = await sdjwt.validate(credential); + console.log('validated:', validated); + + // You can decode the SD JWT to get the payload and the disclosures + const sdJwtToken = await sdjwt.decode(credential); + + // You can get the keys of the claims from the decoded SD JWT + const keys = await sdJwtToken.keys(digest); + console.log({ keys }); + + // You can get the claims from the decoded SD JWT + const payloads = await sdJwtToken.getClaims(digest); + + // You can get the presentable keys from the decoded SD JWT + const presentableKeys = await sdJwtToken.presentableKeys(digest); + + console.log({ + payloads: JSON.stringify(payloads, null, 2), + disclosures: JSON.stringify(sdJwtToken.disclosures, null, 2), + claim: JSON.stringify(sdJwtToken.jwt?.payload, null, 2), + presentableKeys, + }); + + console.log( + '================================================================', + ); + + // Holder Define the presentation frame to specify which claims should be presented + // The list of presented claims must be a subset of the disclosed claims + // the presentation frame is determined by the verifier or the protocol that was agreed upon between the holder and the verifier + const presentationFrame = ['firstname', 'id']; + + // Create a presentation using the issued credential and the presentation frame + // return a Encoded SD JWT. Holder send the presentation to the verifier + const presentation = await sdjwt.present(credential, presentationFrame); + console.log('presentedSDJwt:', presentation); + + // Verifier Define the required claims that need to be verified in the presentation + const requiredClaims = ['firstname', 'id', 'data.ssn']; + + // Verify the presentation using the public key and the required claims + // return a boolean result + const verified = await sdjwt.verify(credential, requiredClaims); + console.log('verified:', verified); +})(); diff --git a/examples/sd-jwt-example/basic.ts b/examples/sd-jwt-example/basic.ts new file mode 100644 index 0000000..9f25dd1 --- /dev/null +++ b/examples/sd-jwt-example/basic.ts @@ -0,0 +1,55 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { DisclosureFrame } from '@sd-jwt/types'; +import { createSignerVerifier, digest, generateSalt } from './utils'; + +(async () => { + const { signer, verifier } = await createSignerVerifier(); + + // Create SDJwt instance for use + const sdjwt = new SDJwtInstance({ + signer, + verifier, + signAlg: 'EdDSA', + hasher: digest, + hashAlg: 'SHA-256', + saltGenerator: generateSalt, + }); + + // Issuer Define the claims object with the user's information + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + + // Issuer Define the disclosure frame to specify which claims can be disclosed + const disclosureFrame: DisclosureFrame = { + _sd: ['firstname', 'lastname', 'ssn'], + }; + + // Issue a signed JWT credential with the specified claims and disclosures + // Return a Encoded SD JWT. Issuer send the credential to the holder + const credential = await sdjwt.issue(claims, disclosureFrame); + + // Holder Receive the credential from the issuer and validate it + // Return a boolean result + const valid = await sdjwt.validate(credential); + + // Holder Define the presentation frame to specify which claims should be presented + // The list of presented claims must be a subset of the disclosed claims + // the presentation frame is determined by the verifier or the protocol that was agreed upon between the holder and the verifier + const presentationFrame = ['firstname', 'ssn']; + + // Create a presentation using the issued credential and the presentation frame + // return a Encoded SD JWT. Holder send the presentation to the verifier + const presentation = await sdjwt.present(credential, presentationFrame); + + // Verifier Define the required claims that need to be verified in the presentation + const requiredClaims = ['firstname', 'ssn', 'id']; + + // Verify the presentation using the public key and the required claims + // return a boolean result + const verified = await sdjwt.verify(presentation, requiredClaims); + console.log(verified); +})(); diff --git a/examples/sd-jwt-example/custom.ts b/examples/sd-jwt-example/custom.ts new file mode 100644 index 0000000..ae16215 --- /dev/null +++ b/examples/sd-jwt-example/custom.ts @@ -0,0 +1,82 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { DisclosureFrame } from '@sd-jwt/types'; +import { createSignerVerifier, digest, generateSalt } from './utils'; + +(async () => { + const { signer, verifier } = await createSignerVerifier(); + + // Create SDJwt instance for use + const sdjwt = new SDJwtInstance({ + signer, + verifier, + signAlg: 'EdDSA', + hasher: digest, + hashAlg: 'SHA-256', + saltGenerator: generateSalt, + }); + + // Issuer Define the claims object with the user's information + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + + // Issuer Define the disclosure frame to specify which claims can be disclosed + const disclosureFrame: DisclosureFrame = { + _sd: ['firstname', 'id'], + }; + + // Issue a signed JWT credential with the specified claims and disclosures + // Return a Encoded SD JWT. Issuer send the credential to the holder + const credential = await sdjwt.issue(claims, disclosureFrame); + console.log('encodedJwt:', credential); + + // Holder Receive the credential from the issuer and validate it + // Return a boolean result + const validated = await sdjwt.validate(credential); + console.log('validated:', validated); + + // You can decode the SD JWT to get the payload and the disclosures + const sdJwtToken = await sdjwt.decode(credential); + + // You can get the keys of the claims from the decoded SD JWT + const keys = await sdJwtToken.keys(digest); + console.log({ keys }); + + // You can get the claims from the decoded SD JWT + const payloads = await sdJwtToken.getClaims(digest); + + // You can get the presentable keys from the decoded SD JWT + const presentableKeys = await sdJwtToken.presentableKeys(digest); + + console.log({ + payloads: JSON.stringify(payloads, null, 2), + disclosures: JSON.stringify(sdJwtToken.disclosures, null, 2), + claim: JSON.stringify(sdJwtToken.jwt?.payload, null, 2), + presentableKeys, + }); + + console.log( + '================================================================', + ); + + // Holder Define the presentation frame to specify which claims should be presented + // The list of presented claims must be a subset of the disclosed claims + // the presentation frame is determined by the verifier or the protocol that was agreed upon between the holder and the verifier + const presentationFrame = ['firstname', 'id']; + + // Create a presentation using the issued credential and the presentation frame + // return a Encoded SD JWT. Holder send the presentation to the verifier + const presentation = await sdjwt.present(credential, presentationFrame); + console.log('presentedSDJwt:', presentation); + + // Verifier Define the required claims that need to be verified in the presentation + const requiredClaims = ['firstname', 'id']; + + // Verify the presentation using the public key and the required claims + // return a boolean result + const verified = await sdjwt.verify(presentation, requiredClaims); + console.log('verified:', verified); +})(); diff --git a/examples/sd-jwt-example/custom_header.ts b/examples/sd-jwt-example/custom_header.ts new file mode 100644 index 0000000..c01c96c --- /dev/null +++ b/examples/sd-jwt-example/custom_header.ts @@ -0,0 +1,41 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { DisclosureFrame } from '@sd-jwt/types'; +import { createSignerVerifier, digest, generateSalt } from './utils'; + +(async () => { + const { signer, verifier } = await createSignerVerifier(); + + // Create SDJwt instance for use + const sdjwt = new SDJwtInstance({ + signer, + verifier, + signAlg: 'EdDSA', + hasher: digest, + hashAlg: 'SHA-256', + saltGenerator: generateSalt, + }); + + // Issuer Define the claims object with the user's information + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + + // Issuer Define the disclosure frame to specify which claims can be disclosed + const disclosureFrame: DisclosureFrame = { + _sd: ['firstname', 'id'], + }; + + // Issue a signed JWT credential with the specified claims and disclosures + // Return a Encoded SD JWT. Issuer send the credential to the holder + const credential = await sdjwt.issue(claims, disclosureFrame, { + header: { typ: 'vc+sd-jwt', custom: 'data' }, // You can add custom header data to the SD JWT + }); + console.log('encodedSdjwt:', credential); + + // You can check the custom header data by decoding the SD JWT + const sdJwtToken = await sdjwt.decode(credential); + console.log(sdJwtToken); +})(); diff --git a/examples/sd-jwt-example/decode.ts b/examples/sd-jwt-example/decode.ts new file mode 100644 index 0000000..a14247b --- /dev/null +++ b/examples/sd-jwt-example/decode.ts @@ -0,0 +1,27 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { createSignerVerifier, digest, generateSalt } from './utils'; + +(async () => { + const { signer, verifier } = await createSignerVerifier(); + + // Create SDJwt instance for use + const sdjwt = new SDJwtInstance({ + signer, + signAlg: 'EdDSA', + verifier, + hasher: digest, + saltGenerator: generateSalt, + kbSigner: signer, + kbSignAlg: 'EdDSA', + kbVerifier: verifier, + }); + + // this is an example of SD JWT + const data = + 'eyJhbGciOiAiRVMyNTYiLCAia2lkIjogImRvYy1zaWduZXItMDUtMjUtMjAyMiIsICJ0eXAiOiAidmMrc2Qtand0In0.eyJfc2QiOiBbIjA5dktySk1PbHlUV00wc2pwdV9wZE9CVkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9kYXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9pZUxaaGg3WGJVVHRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNONndxUVhDaXciLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiamRyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5eVZ1bHdRUWxoRmxNXzNKbHpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJkY3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxlLmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.b036DutqQ72WszrCq0GuqZnbws3MApQyzA41I5DSJmenUfsADtqW8FbI_N04FP1wZDF_JtV6a6Ke3Z7apkoTLA~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImlzX292ZXJfMTgiLCB0cnVlXQ~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImlzX292ZXJfMjEiLCB0cnVlXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImlzX292ZXJfNjUiLCB0cnVlXQ~'; + const decodedObject = await sdjwt.decode(data); + console.log(decodedObject); + + const claims = await sdjwt.getClaims(data); + console.log(claims); +})(); diff --git a/examples/sd-jwt-example/decoy.ts b/examples/sd-jwt-example/decoy.ts new file mode 100644 index 0000000..e146d7e --- /dev/null +++ b/examples/sd-jwt-example/decoy.ts @@ -0,0 +1,35 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { DisclosureFrame } from '@sd-jwt/types'; +import { createSignerVerifier, digest, generateSalt } from './utils'; + +(async () => { + const { signer, verifier } = await createSignerVerifier(); + + // Create SDJwt instance for use + const sdjwt = new SDJwtInstance({ + signer, + verifier, + signAlg: 'EdDSA', + hasher: digest, + hashAlg: 'SHA-256', + saltGenerator: generateSalt, + }); + // Issuer Define the claims object with the user's information + const claims = { + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + + // Issuer Define the disclosure frame to specify which claims can be disclosed + const disclosureFrame: DisclosureFrame = { + _sd: ['id'], + _sd_decoy: 1, // 1 decoy digest will be added in SD JWT + }; + const credential = await sdjwt.issue(claims, disclosureFrame); + console.log('encodedSdjwt:', credential); + + // You can check the decoy digest in the SD JWT by decoding it + const sdJwtToken = await sdjwt.decode(credential); + console.log(sdJwtToken); +})(); diff --git a/examples/sd-jwt-example/kb.ts b/examples/sd-jwt-example/kb.ts new file mode 100644 index 0000000..934a6c5 --- /dev/null +++ b/examples/sd-jwt-example/kb.ts @@ -0,0 +1,49 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { DisclosureFrame } from '@sd-jwt/types'; +import { createSignerVerifier, digest, generateSalt } from './utils'; + +(async () => { + const { signer, verifier } = await createSignerVerifier(); + + // Create SDJwt instance for use + const sdjwt = new SDJwtInstance({ + signer, + signAlg: 'EdDSA', + verifier, + hasher: digest, + saltGenerator: generateSalt, + kbSigner: signer, + kbSignAlg: 'EdDSA', + kbVerifier: verifier, + }); + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + const disclosureFrame: DisclosureFrame = { + _sd: ['firstname', 'id'], + }; + + const kbPayload = { + iat: Math.floor(Date.now() / 1000), + aud: 'https://example.com', + nonce: '1234', + custom: 'data', + }; + + const encodedSdjwt = await sdjwt.issue(claims, disclosureFrame); + console.log('encodedSdjwt:', encodedSdjwt); + const sdjwttoken = await sdjwt.decode(encodedSdjwt); + console.log(sdjwttoken); + + const presentedSdJwt = await sdjwt.present(encodedSdjwt, ['id'], { + kb: { + payload: kbPayload, + }, + }); + + const verified = await sdjwt.verify(presentedSdJwt, ['id', 'ssn'], true); + console.log(verified); +})(); diff --git a/examples/sd-jwt-example/package.json b/examples/sd-jwt-example/package.json new file mode 100644 index 0000000..4fe2cf8 --- /dev/null +++ b/examples/sd-jwt-example/package.json @@ -0,0 +1,30 @@ +{ + "name": "sdjwt-examples", + "version": "1.0.0", + "description": "", + "main": "index.js", + "private": true, + "scripts": { + "basic": "ts-node basic.ts", + "all": "ts-node all.ts", + "sdjwtobject": "ts-node sdjwtobject.ts", + "custom": "ts-node custom.ts", + "decoy": "ts-node decoy.ts", + "custom_header": "ts-node custom_header.ts", + "kb": "ts-node kb.ts", + "decode": "ts-node decode.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/node": "^20.10.4", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + }, + "dependencies": { + "@sd-jwt/core": "workspace:*", + "@sd-jwt/types": "workspace:*", + "@sd-jwt/crypto-nodejs": "workspace:*" + } +} diff --git a/examples/sd-jwt-example/sdjwtobject.ts b/examples/sd-jwt-example/sdjwtobject.ts new file mode 100644 index 0000000..3535134 --- /dev/null +++ b/examples/sd-jwt-example/sdjwtobject.ts @@ -0,0 +1,57 @@ +import { SDJwtInstance } from '@sd-jwt/core'; +import { DisclosureFrame } from '@sd-jwt/types'; +import { createSignerVerifier, digest, generateSalt } from './utils'; + +(async () => { + const { signer, verifier } = await createSignerVerifier(); + + // Create SDJwt instance for use + const sdjwt = new SDJwtInstance({ + signer, + signAlg: 'EdDSA', + verifier, + hasher: digest, + saltGenerator: generateSalt, + kbSigner: signer, + kbSignAlg: 'EdDSA', + kbVerifier: verifier, + }); + // Issuer Define the claims object with the user's information + const claims = { + firstname: 'John', + lastname: 'Doe', + ssn: '123-45-6789', + id: '1234', + }; + + // Issuer Define the disclosure frame to specify which claims can be disclosed + const disclosureFrame: DisclosureFrame = { + _sd: ['firstname', 'id'], + }; + + // Issue a signed JWT credential with the specified claims and disclosures + // Return a Encoded SD JWT. Issuer send the credential to the holder + const credential = await sdjwt.issue(claims, disclosureFrame); + console.log('encodedSdjwt:', credential); + + // You can decode the SD JWT to get the payload and the disclosures + const sdJwtToken = await sdjwt.decode(credential); + console.log(sdJwtToken); + + // You can get the keys of the claims from the decoded SD JWT + const keys = await sdJwtToken.keys(digest); + console.log({ keys }); + + // You can get the claims from the decoded SD JWT + const payloads = await sdJwtToken.getClaims(digest); + + // You can get the presentable keys from the decoded SD JWT + const presentableKeys = await sdJwtToken.presentableKeys(digest); + + console.log({ + payloads: JSON.stringify(payloads, null, 2), + disclosures: JSON.stringify(sdJwtToken.disclosures, null, 2), + claim: JSON.stringify(sdJwtToken.jwt?.payload, null, 2), + presentableKeys, + }); +})(); diff --git a/examples/sd-jwt-example/tsconfig.json b/examples/sd-jwt-example/tsconfig.json new file mode 100644 index 0000000..940ce53 --- /dev/null +++ b/examples/sd-jwt-example/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "references": [ + { + "path": "../../packages/core" + } + ] +} diff --git a/examples/sd-jwt-example/utils.ts b/examples/sd-jwt-example/utils.ts new file mode 100644 index 0000000..3eb5426 --- /dev/null +++ b/examples/sd-jwt-example/utils.ts @@ -0,0 +1,10 @@ +import { ES256, digest, generateSalt } from '@sd-jwt/crypto-nodejs'; +export { digest, generateSalt }; + +export const createSignerVerifier = async () => { + const { privateKey, publicKey } = await ES256.generateKeyPair(); + return { + signer: await ES256.getSigner(privateKey), + verifier: await ES256.getVerifier(publicKey), + }; +}; diff --git a/packages/core/README.md b/packages/core/README.md index a8693ea..c80089a 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -32,7 +32,8 @@ Ensure you have Node.js installed as a prerequisite. ### Usage -This library can not be used on it's own, it is a dependency for other implementations like `@sd-jwt/sd-jwt-vc`. +The library can be used to create sd-jwt based credentials. To be compliant with the `sd-jwt-vc` standard, you can use the `@sd-jwt/sd-jwt-vc` that is implementing this spec. +If you want to use the pure sd-jwt class or implement your own sd-jwt credential approach, you can use this library. ### Dependencies diff --git a/packages/core/package.json b/packages/core/package.json index 73dddfc..ba64f66 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -14,10 +14,9 @@ "scripts": { "build": "rm -rf **/dist && tsup", "lint": "biome lint ./src", - "test": "pnpm run test:node && pnpm run test:browser && pnpm run test:e2e && pnpm run test:cov", - "test:node": "vitest run ./src/test/*.spec.ts && vitest run ./src/test/*.spec.ts --environment jsdom", + "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:e2e": "vitest run ./test/*e2e.spec.ts --environment node", "test:cov": "vitest run --coverage" }, "keywords": [ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3060948..8cdec5a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,9 +19,9 @@ export * from './decoy'; export type SdJwtPayload = Record; -export abstract class SDJwtInstance { +export class SDJwtInstance { //header type - protected abstract type: string; + protected type?: string; public static DEFAULT_hashAlg = 'sha-256'; @@ -127,9 +127,16 @@ export abstract class SDJwtInstance { return sdJwt.encodeSDJwt(); } - protected abstract validateReservedFields( + /** + * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error. + * @param disclosureFrame + * @returns + */ + protected validateReservedFields( disclosureFrame: DisclosureFrame, - ): void; + ) { + return; + } public async present( encodedSDJwt: string, diff --git a/packages/core/src/test/index.spec.ts b/packages/core/src/test/index.spec.ts index c672235..d82d5e3 100644 --- a/packages/core/src/test/index.spec.ts +++ b/packages/core/src/test/index.spec.ts @@ -1,19 +1,9 @@ import { SDJwtInstance, SdJwtPayload } from '../index'; -import { DisclosureFrame, Signer, Verifier } from '@sd-jwt/types'; +import { Signer, Verifier } from '@sd-jwt/types'; import Crypto from 'node:crypto'; import { describe, expect, test } from 'vitest'; import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; -export class TestInstance extends SDJwtInstance { - protected type = 'sd-jwt'; - - protected validateReservedFields( - disclosureFrame: DisclosureFrame, - ): void { - return; - } -} - export const createSignerVerifier = () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); const signer: Signer = async (data: string) => { @@ -33,13 +23,13 @@ export const createSignerVerifier = () => { describe('index', () => { test('create', async () => { - const sdjwt = new TestInstance(); + const sdjwt = new SDJwtInstance(); expect(sdjwt).toBeDefined(); }); test('kbJwt', async () => { const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, signAlg: 'EdDSA', verifier, @@ -77,7 +67,7 @@ describe('index', () => { test('issue', async () => { const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, signAlg: 'EdDSA', verifier, @@ -111,7 +101,7 @@ describe('index', () => { ); }; - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, signAlg: 'EdDSA', verifier: failedverifier, @@ -149,7 +139,7 @@ describe('index', () => { Buffer.from(sig, 'base64url'), ); }; - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, signAlg: 'EdDSA', verifier, @@ -191,7 +181,7 @@ describe('index', () => { test('verify with kbJwt', async () => { const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, signAlg: 'EdDSA', verifier, @@ -229,7 +219,7 @@ describe('index', () => { }); test('Hasher not found', async () => { - const sdjwt = new TestInstance({}); + const sdjwt = new SDJwtInstance({}); try { const credential = await sdjwt.issue( { @@ -250,7 +240,7 @@ describe('index', () => { }); test('SaltGenerator not found', async () => { - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ hasher: digest, }); try { @@ -273,7 +263,7 @@ describe('index', () => { }); test('Signer not found', async () => { - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ hasher: digest, saltGenerator: generateSalt, }); @@ -298,7 +288,7 @@ describe('index', () => { test('Verifier not found', async () => { const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, hasher: digest, saltGenerator: generateSalt, @@ -338,7 +328,7 @@ describe('index', () => { test('kbSigner not found', async () => { const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, verifier, hasher: digest, @@ -376,7 +366,7 @@ describe('index', () => { test('kbVerifier not found', async () => { const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, verifier, hasher: digest, @@ -416,7 +406,7 @@ describe('index', () => { test('kbSignAlg not found', async () => { const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, verifier, hasher: digest, @@ -440,7 +430,6 @@ describe('index', () => { const presentation = sdjwt.present(credential, ['foo'], { kb: { payload: { - sd_hash: 'sha-256', aud: '1', iat: 1, nonce: '342', @@ -452,8 +441,56 @@ describe('index', () => { ); }); - test('hasher is not found', () => { - const sdjwt = new TestInstance({}); + test('hasher is not found', async () => { + const { signer } = createSignerVerifier(); + const sdjwt_create = new SDJwtInstance({ + signer, + hasher: digest, + saltGenerator: generateSalt, + signAlg: 'EdDSA', + }); + const credential = await sdjwt_create.issue( + { + foo: 'bar', + iss: 'Issuer', + iat: new Date().getTime(), + vct: '', + }, + { + _sd: ['foo'], + }, + ); + const sdjwt = new SDJwtInstance({}); expect(sdjwt.keys('')).rejects.toThrow('Hasher not found'); + expect(sdjwt.presentableKeys('')).rejects.toThrow('Hasher not found'); + expect(sdjwt.getClaims('')).rejects.toThrow('Hasher not found'); + expect(() => sdjwt.decode('')).toThrowError('Hasher not found'); + expect(sdjwt.present(credential, ['foo'])).rejects.toThrow( + 'Hasher not found', + ); + }); + + test('presentableKeys', async () => { + const { signer } = createSignerVerifier(); + const sdjwt = new SDJwtInstance({ + signer, + hasher: digest, + saltGenerator: generateSalt, + signAlg: 'EdDSA', + }); + const credential = await sdjwt.issue( + { + foo: 'bar', + iss: 'Issuer', + iat: new Date().getTime(), + vct: '', + }, + { + _sd: ['foo'], + }, + ); + const keys = await sdjwt.presentableKeys(credential); + expect(keys).toBeDefined(); + expect(keys).toEqual(['foo']); }); }); diff --git a/packages/core/test/app-e2e.spec.ts b/packages/core/test/app-e2e.spec.ts index d8440be..1734870 100644 --- a/packages/core/test/app-e2e.spec.ts +++ b/packages/core/test/app-e2e.spec.ts @@ -1,11 +1,10 @@ import Crypto from 'node:crypto'; -import { SdJwtPayload } from '../src'; -import { DisclosureFrame, SD, Signer, Verifier } from '@sd-jwt/types'; +import { SDJwtInstance, SdJwtPayload } from '../src'; +import { DisclosureFrame, Signer, Verifier } from '@sd-jwt/types'; import fs from 'fs'; import path from 'path'; import { describe, expect, test } from 'vitest'; import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; -import { TestInstance } from '../src/test/index.spec'; export const createSignerVerifier = () => { const { privateKey, publicKey } = Crypto.generateKeyPairSync('ed25519'); @@ -27,7 +26,7 @@ export const createSignerVerifier = () => { describe('App', () => { test('Example', async () => { const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, signAlg: 'EdDSA', verifier, @@ -193,7 +192,7 @@ describe('App', () => { async function JSONtest(filename: string) { const test = loadTestJsonFile(filename); const { signer, verifier } = createSignerVerifier(); - const sdjwt = new TestInstance({ + const sdjwt = new SDJwtInstance({ signer, signAlg: 'EdDSA', verifier, @@ -210,7 +209,7 @@ async function JSONtest(filename: string) { expect(validated).toBeDefined(); expect(validated).toStrictEqual({ - header: { alg: 'EdDSA', typ: 'sd-jwt' }, + header: { alg: 'EdDSA' }, payload: test.claims, }); @@ -229,7 +228,7 @@ async function JSONtest(filename: string) { expect(verified).toBeDefined(); expect(verified).toStrictEqual({ - header: { alg: 'EdDSA', typ: 'sd-jwt' }, + header: { alg: 'EdDSA' }, payload: test.claims, }); } diff --git a/packages/decode/src/test/decode.spec.ts b/packages/decode/src/test/decode.spec.ts index a9a41a5..7d35a6f 100644 --- a/packages/decode/src/test/decode.spec.ts +++ b/packages/decode/src/test/decode.spec.ts @@ -5,6 +5,7 @@ import { decodeSdJwtSync, getClaims, getClaimsSync, + getSDAlgAndPayload, splitSdJwt, } from '../index'; import { digest } from '@sd-jwt/crypto-nodejs'; @@ -36,6 +37,14 @@ describe('decode tests', () => { expect(kbJwt).toBeUndefined(); }); + test('split sdjwt without disclosures', () => { + const sdjwt = 'h.p.s'; + const { jwt, disclosures, kbJwt } = splitSdJwt(sdjwt); + expect(jwt).toBe('h.p.s'); + expect(disclosures).toStrictEqual([]); + expect(kbJwt).toBeUndefined(); + }); + test('split sdjwt with kbjwt', () => { const sdjwt = 'h.p.s~d1~d2~kbh.kbp.kbs'; const { jwt, disclosures, kbJwt } = splitSdJwt(sdjwt); @@ -147,4 +156,9 @@ describe('decode tests', () => { }, }); }); + + test('Test default sd hash algorithm', () => { + const { _sd_alg, payload } = getSDAlgAndPayload({}); + expect(_sd_alg).toBe('sha-256'); + }); }); diff --git a/packages/sd-jwt-vc/src/index.ts b/packages/sd-jwt-vc/src/index.ts index af0649e..2cb8fe7 100644 --- a/packages/sd-jwt-vc/src/index.ts +++ b/packages/sd-jwt-vc/src/index.ts @@ -1,30 +1,20 @@ -import { SDJwt, SDJwtInstance, SdJwtPayload } from '@sd-jwt/core'; +import { SDJwtInstance } from '@sd-jwt/core'; import { DisclosureFrame } from '@sd-jwt/types'; import { SDJWTException } from '../../utils/dist'; +import { SdJwtVcPayload } from './sd-jwt-vc-payload'; -export interface SdJwtVcPayload extends SdJwtPayload { - // The Issuer of the Verifiable Credential. The value of iss MUST be a URI. See [RFC7519] for more information. - iss: string; - // The time of issuance of the Verifiable Credential. See [RFC7519] for more information. - iat: number; - // OPTIONAL. The time before which the Verifiable Credential MUST NOT be accepted before validating. See [RFC7519] for more information. - nbf?: number; - //OPTIONAL. The expiry time of the Verifiable Credential after which the Verifiable Credential is no longer valid. See [RFC7519] for more information. - exp?: number; - // REQUIRED when Cryptographic Key Binding is to be supported. Contains the confirmation method as defined in [RFC7800]. It is RECOMMENDED that this contains a JWK as defined in Section 3.2 of [RFC7800]. For Cryptographic Key Binding, the Key Binding JWT in the Combined Format for Presentation MUST be signed by the key identified in this claim. - cnf?: unknown; - //REQUIRED. The type of the Verifiable Credential, e.g., https://credentials.example.com/identity_credential, as defined in Section 3.2.2.1.1. - vct: string; - // OPTIONAL. The information on how to read the status of the Verifiable Credential. See [I-D.looker-oauth-jwt-cwt-status-list] for more information. - status?: unknown; - - //The identifier of the Subject of the Verifiable Credential. The Issuer MAY use it to provide the Subject identifier known by the Issuer. There is no requirement for a binding to exist between sub and cnf claims. - sub?: string; -} +export { SdJwtVcPayload } from './sd-jwt-vc-payload'; export class SDJwtVcInstance extends SDJwtInstance { + /** + * The type of the SD-JWT-VC set in the header.typ field. + */ protected type = 'sd-jwt-vc'; + /** + * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error. + * @param disclosureFrame + */ protected validateReservedFields( disclosureFrame: DisclosureFrame, ): void { diff --git a/packages/sd-jwt-vc/src/sd-jwt-vc-payload.ts b/packages/sd-jwt-vc/src/sd-jwt-vc-payload.ts new file mode 100644 index 0000000..8017684 --- /dev/null +++ b/packages/sd-jwt-vc/src/sd-jwt-vc-payload.ts @@ -0,0 +1,21 @@ +import { SdJwtPayload } from '@sd-jwt/core'; + +export interface SdJwtVcPayload extends SdJwtPayload { + // The Issuer of the Verifiable Credential. The value of iss MUST be a URI. See [RFC7519] for more information. + iss: string; + // The time of issuance of the Verifiable Credential. See [RFC7519] for more information. + iat: number; + // OPTIONAL. The time before which the Verifiable Credential MUST NOT be accepted before validating. See [RFC7519] for more information. + nbf?: number; + //OPTIONAL. The expiry time of the Verifiable Credential after which the Verifiable Credential is no longer valid. See [RFC7519] for more information. + exp?: number; + // REQUIRED when Cryptographic Key Binding is to be supported. Contains the confirmation method as defined in [RFC7800]. It is RECOMMENDED that this contains a JWK as defined in Section 3.2 of [RFC7800]. For Cryptographic Key Binding, the Key Binding JWT in the Combined Format for Presentation MUST be signed by the key identified in this claim. + cnf?: unknown; + //REQUIRED. The type of the Verifiable Credential, e.g., https://credentials.example.com/identity_credential, as defined in Section 3.2.2.1.1. + vct: string; + // OPTIONAL. The information on how to read the status of the Verifiable Credential. See [I-D.looker-oauth-jwt-cwt-status-list] for more information. + status?: unknown; + + //The identifier of the Subject of the Verifiable Credential. The Issuer MAY use it to provide the Subject identifier known by the Issuer. There is no requirement for a binding to exist between sub and cnf claims. + sub?: string; +} diff --git a/packages/sd-jwt-vc/src/test/index.spec.ts b/packages/sd-jwt-vc/src/test/index.spec.ts new file mode 100644 index 0000000..a9e8784 --- /dev/null +++ b/packages/sd-jwt-vc/src/test/index.spec.ts @@ -0,0 +1,38 @@ +import { digest, generateSalt } from '@sd-jwt/crypto-nodejs'; +import { DisclosureFrame } from '@sd-jwt/types'; +import { describe, test, expect } from 'vitest'; +import { SDJwtVcInstance } from '..'; +import { createSignerVerifier } from '../../test/app-e2e.spec'; +import { SdJwtVcPayload } from '../sd-jwt-vc-payload'; + +const iss = 'ExampleIssuer'; +const vct = 'https://example.com/schema/1'; +const iat = new Date().getTime() / 1000; + +describe('App', () => { + test('Example', async () => { + const { signer, verifier } = createSignerVerifier(); + const sdjwt = new SDJwtVcInstance({ + signer, + signAlg: 'EdDSA', + verifier, + hasher: digest, + hashAlg: 'SHA-256', + saltGenerator: generateSalt, + }); + + const claims = { + firstname: 'John', + }; + const disclosureFrame = { + _sd: ['firstname', 'iss'], + }; + + const expectedPayload: SdJwtVcPayload = { iat, iss, vct, ...claims }; + const encodedSdjwt = sdjwt.issue( + expectedPayload, + disclosureFrame as unknown as DisclosureFrame, + ); + expect(encodedSdjwt).rejects.toThrowError(); + }); +}); diff --git a/packages/sd-jwt-vc/test/app-e2e.spec.ts b/packages/sd-jwt-vc/test/app-e2e.spec.ts index 64a9661..00fc604 100644 --- a/packages/sd-jwt-vc/test/app-e2e.spec.ts +++ b/packages/sd-jwt-vc/test/app-e2e.spec.ts @@ -1,5 +1,5 @@ import Crypto from 'node:crypto'; -import { SDJwtVcInstance } from '../src'; +import { SDJwtVcInstance, SdJwtVcPayload } from '../src/index'; import { DisclosureFrame, Signer, Verifier } from '@sd-jwt/types'; import fs from 'fs'; import path from 'path'; @@ -72,7 +72,7 @@ describe('App', () => { }, }; - const expectedPayload = { iat, iss, vct, ...claims }; + const expectedPayload: SdJwtVcPayload = { iat, iss, vct, ...claims }; const encodedSdjwt = await sdjwt.issue(expectedPayload, disclosureFrame); expect(encodedSdjwt).toBeDefined(); const validated = await sdjwt.validate(encodedSdjwt); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7953b1..e31252e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,28 @@ importers: specifier: ^5.3.3 version: 5.3.3 + examples/sd-jwt-example: + dependencies: + '@sd-jwt/core': + specifier: workspace:* + version: link:../../packages/core + '@sd-jwt/crypto-nodejs': + specifier: workspace:* + version: link:../../packages/node-crypto + '@sd-jwt/types': + specifier: workspace:* + version: link:../../packages/types + devDependencies: + '@types/node': + specifier: ^20.10.4 + version: 20.11.20 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.11.20)(typescript@5.3.3) + typescript: + specifier: ^5.3.3 + version: 5.3.3 + examples/sd-jwt-vc-example: dependencies: '@sd-jwt/crypto-nodejs':