From 6324f978ae1b08c5dd5e116129166f40c8e3a58f Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Thu, 3 Oct 2024 00:04:23 +0200 Subject: [PATCH] feat: JWE JWT compact agent methods --- packages/jwt-service/__tests__/jwe.test.ts | 54 +++ .../__tests__/shared/jwtServiceTest.ts | 216 ++++++----- packages/jwt-service/package.json | 2 + packages/jwt-service/src/agent/JwtService.ts | 143 ++++--- packages/jwt-service/src/functions/JWE.ts | 358 ++++++++++++++++++ packages/jwt-service/src/functions/index.ts | 30 +- packages/jwt-service/src/index.ts | 4 + packages/jwt-service/src/types/IJwtService.ts | 315 +++++++++++---- .../src/agent/SphereonKeyManager.ts | 4 +- .../src/types/ISphereonKeyManager.ts | 2 +- pnpm-lock.yaml | 14 +- 11 files changed, 909 insertions(+), 233 deletions(-) create mode 100644 packages/jwt-service/__tests__/jwe.test.ts create mode 100644 packages/jwt-service/src/functions/JWE.ts diff --git a/packages/jwt-service/__tests__/jwe.test.ts b/packages/jwt-service/__tests__/jwe.test.ts new file mode 100644 index 00000000..0d2f473f --- /dev/null +++ b/packages/jwt-service/__tests__/jwe.test.ts @@ -0,0 +1,54 @@ +import * as u8a from 'uint8arrays' +import {CompactJwtEncrypter} from "../src/functions/JWE"; + +describe('JWE test', () => { + + const ietfPublicJwk = { + kty: 'EC', + crv: 'P-256', + x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', + y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', + alg: "ECDH-ES", + "use": "enc", + // d: 'jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI', + } + + const ietfPrivateJwk = { + ...ietfPublicJwk, + d: 'jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI', + } + + + it('should encrypt', async () => { + const pubKey = await crypto.subtle.importKey('jwk', ietfPublicJwk, { + name: 'ECDH', + namedCurve: 'P-256', + }, true, []) + const encrypter = new CompactJwtEncrypter({ + alg: 'ECDH-ES', + enc: 'A256GCM', + keyManagementParams: {apu: u8a.fromString('apu'), apv: u8a.fromString('apv')}, + key: pubKey + }) + + const encrypted = await encrypter.encryptCompactJWT({'hello': 'world'}, {}) + console.log(encrypted) + + const secKey = await crypto.subtle.importKey('jwk', ietfPrivateJwk, { + name: 'ECDH', + namedCurve: 'P-256', + }, true, ["deriveKey", "deriveBits"]) + const decrypted = await CompactJwtEncrypter.decryptCompactJWT(encrypted, secKey ) + console.log(JSON.stringify(decrypted, null, 2)) + }) + + it ('should decrypt agent example', async () => { + const jwe = 'eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJ4IjoiYkIza0VMaWFtOTBEWExKVU8zQXFCa3RSMmd3TVFWSFBEWUJWUkJ3NEpWWSIsImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ5IjoiMXVNRTFlWHJQVjR2VVhiZHNYRGpBNno2NGMyYmQ3M0stWWtBVHlRRzNrTSJ9LCJhcHUiOiJZWEIxIiwiYXB2IjoiWVhCMiJ9..gT7grdO892xezIiy.mzWRiE0ajMnqVqVRs3medXCtH4knMBLGWWaPTap8CwCw_TpkVSV2azzz7MsTz6pjGo5iDHWa_AMxuGRCTZVBew.S5WfGjVhFnFwgqPtYdBJzQ' + const secKey = await crypto.subtle.importKey('jwk', ietfPrivateJwk, { + name: 'ECDH', + namedCurve: 'P-256', + }, true, ["deriveKey", "deriveBits"]) + const decrypted = await CompactJwtEncrypter.decryptCompactJWT(jwe, secKey ) + console.log(JSON.stringify(decrypted, null, 2)) + }) +}) diff --git a/packages/jwt-service/__tests__/shared/jwtServiceTest.ts b/packages/jwt-service/__tests__/shared/jwtServiceTest.ts index f8ab8638..3a4b20e9 100644 --- a/packages/jwt-service/__tests__/shared/jwtServiceTest.ts +++ b/packages/jwt-service/__tests__/shared/jwtServiceTest.ts @@ -1,98 +1,134 @@ -import { IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { JWK } from '@sphereon/ssi-types' -import { IDIDManager, IKeyManager, TAgent } from '@veramo/core' -import { IJwtService } from '../../src' +import {IIdentifierResolution} from '@sphereon/ssi-sdk-ext.identifier-resolution' +import {JWK} from '@sphereon/ssi-types' +import {IDIDManager, IKeyManager, TAgent} from '@veramo/core' +import {decodeJwt} from "jose"; + +import * as u8a from 'uint8arrays' +import {IJwtService} from '../../src' type ConfiguredAgent = TAgent -export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Promise; tearDown: () => Promise }) => { - let agent: ConfiguredAgent - // let key: IKey - - const ietfJwk = { - kty: 'EC', - crv: 'P-256', - x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', - y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', - // d: 'jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI', - } - // tbe above key as hex - const privateKeyHex = '8E9B109E719098BF980487DF1F5D77E9CB29606EBED2263B5F57C213DF84F4B2'.toLowerCase() - const publicKeyHex = '037fcdce2770f6c45d4183cbee6fdb4b7b580733357be9ef13bacf6e3c7bd15445' - const kid = publicKeyHex - - beforeAll(async () => { - await testContext.setup().then(() => (agent = testContext.getAgent())) - await agent.keyManagerImport({ kid: 'test', type: 'Secp256r1', kms: 'local', privateKeyHex }) - }) - afterAll(testContext.tearDown) - - describe('jwt-service', () => { - it('should sign with ietf key', async () => { - const jwt = await agent.jwtCreateJwsCompactSignature({ - // Example payloads from IETF spec - issuer: { identifier: kid, noIdentifierInHeader: true }, - protectedHeader: { alg: 'ES256' }, - payload: 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ', - }) - - const [header, payload, signature] = jwt.jwt.split('.') - expect(header).toStrictEqual('eyJhbGciOiJFUzI1NiJ9') - expect(payload).toStrictEqual('eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ') - // ES256 uses a nonce, so the signature will never be the same as the ietf version - expect(signature).toEqual('e4ZrhZdbFQ7630Tq51E6RQiJaae9bFNGJszIhtusEwzvO21rzH76Wer6yRn2Zb34VjIm3cVRl0iQctbf4uBY3w') +export default (testContext: { + getAgent: () => ConfiguredAgent; + setup: () => Promise; + tearDown: () => Promise +}) => { + let agent: ConfiguredAgent + // let key: IKey + + const ietfJwk = { + kty: 'EC', + crv: 'P-256', + x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', + y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', + // d: 'jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI', + } + // tbe above key as hex + const privateKeyHex = '8E9B109E719098BF980487DF1F5D77E9CB29606EBED2263B5F57C213DF84F4B2'.toLowerCase() + const publicKeyHex = '037fcdce2770f6c45d4183cbee6fdb4b7b580733357be9ef13bacf6e3c7bd15445' + const kid = publicKeyHex + + beforeAll(async () => { + await testContext.setup().then(() => (agent = testContext.getAgent())) + await agent.keyManagerImport({kid: 'test', type: 'Secp256r1', kms: 'local', privateKeyHex}) }) + afterAll(testContext.tearDown) + + describe('jwt-service', () => { + it('should sign with ietf key', async () => { + const jwt = await agent.jwtCreateJwsCompactSignature({ + // Example payloads from IETF spec + issuer: {identifier: kid, noIdentifierInHeader: true}, + protectedHeader: {alg: 'ES256'}, + payload: 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ', + }) + + const [header, payload, signature] = jwt.jwt.split('.') + expect(header).toStrictEqual('eyJhbGciOiJFUzI1NiJ9') + expect(payload).toStrictEqual('eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ') + // ES256 uses a nonce, so the signature will never be the same as the ietf version + expect(signature).toEqual('e4ZrhZdbFQ7630Tq51E6RQiJaae9bFNGJszIhtusEwzvO21rzH76Wer6yRn2Zb34VjIm3cVRl0iQctbf4uBY3w') + }) + + it('should verify with ietf jwk', async () => { + const jwt = await agent.jwtCreateJwsCompactSignature({ + // Example payloads from IETF spec + issuer: {identifier: kid, noIdentifierInHeader: true}, + protectedHeader: {alg: 'ES256'}, + payload: 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ', + }) + + const result = await agent.jwtVerifyJwsSignature({ + jws: jwt.jwt, + jwk: ietfJwk as JWK, + }) - it('should verify with ietf jwk', async () => { - const jwt = await agent.jwtCreateJwsCompactSignature({ - // Example payloads from IETF spec - issuer: { identifier: kid, noIdentifierInHeader: true }, - protectedHeader: { alg: 'ES256' }, - payload: 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ', - }) - - const result = await agent.jwtVerifyJwsSignature({ - jws: jwt.jwt, - jwk: ietfJwk as JWK, - }) - - expect(result).toMatchObject({ - critical: false, - error: false, - jws: { - payload: 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ', - signatures: [ - { - identifier: { - jwk: { - crv: 'P-256', - kty: 'EC', - x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', - y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', + expect(result).toMatchObject({ + critical: false, + error: false, + jws: { + payload: 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ', + signatures: [ + { + identifier: { + jwk: { + crv: 'P-256', + kty: 'EC', + x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', + y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', + }, + jwks: [ + { + jwk: { + crv: 'P-256', + kty: 'EC', + x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', + y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', + }, + jwkThumbprint: 'oKIywvGUpTVTyxMQ3bwIIeQUudfr_CkLMjCE19ECD-U', + publicKeyHex: '037fcdce2770f6c45d4183cbee6fdb4b7b580733357be9ef13bacf6e3c7bd15445', + }, + ], + method: 'jwk', + }, + protected: 'eyJhbGciOiJFUzI1NiJ9', + signature: 'e4ZrhZdbFQ7630Tq51E6RQiJaae9bFNGJszIhtusEwzvO21rzH76Wer6yRn2Zb34VjIm3cVRl0iQctbf4uBY3w', + }, + ], }, - jwks: [ - { - jwk: { - crv: 'P-256', - kty: 'EC', - x: 'f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU', - y: 'x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0', - }, - jwkThumbprint: 'oKIywvGUpTVTyxMQ3bwIIeQUudfr_CkLMjCE19ECD-U', - publicKeyHex: '037fcdce2770f6c45d4183cbee6fdb4b7b580733357be9ef13bacf6e3c7bd15445', - }, - ], - method: 'jwk', - }, - protected: 'eyJhbGciOiJFUzI1NiJ9', - signature: 'e4ZrhZdbFQ7630Tq51E6RQiJaae9bFNGJszIhtusEwzvO21rzH76Wer6yRn2Zb34VjIm3cVRl0iQctbf4uBY3w', - }, - ], - }, - message: 'Signature validated', - name: 'jws', - // verificationTime: expect.any(Date), - }) + message: 'Signature validated', + name: 'jws', + // verificationTime: expect.any(Date), + }) + }) + + + it('should encrypt with public key', async () => { + const jwt = await agent.jwtCreateJwsCompactSignature({ + // Example payloads from IETF spec + issuer: {identifier: kid, noIdentifierInHeader: true}, + protectedHeader: {alg: 'ES256'}, + payload: 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ', + }) + + const [header, payload, signature] = jwt.jwt.split('.') + expect(header).toStrictEqual('eyJhbGciOiJFUzI1NiJ9') + expect(payload).toStrictEqual('eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ') + // ES256 uses a nonce, so the signature will never be the same as the ietf version + expect(signature).toEqual('e4ZrhZdbFQ7630Tq51E6RQiJaae9bFNGJszIhtusEwzvO21rzH76Wer6yRn2Zb34VjIm3cVRl0iQctbf4uBY3w') + + + const jwe = await agent.jwtEncryptJweCompactJwt({ + alg: "ECDH-ES", + enc: "A256GCM", + payload: decodeJwt(jwt.jwt), + keyManagementParams: {apu: u8a.fromString('apu'), apv: u8a.fromString('apv')}, + // @ts-ignore + recipientKey: await agent.identifierExternalResolveByJwk({identifier: ietfJwk}) + }) + + console.log(jwe) + }) }) - }) + } diff --git a/packages/jwt-service/package.json b/packages/jwt-service/package.json index 06f15666..44e91bfc 100644 --- a/packages/jwt-service/package.json +++ b/packages/jwt-service/package.json @@ -22,6 +22,8 @@ "@sphereon/ssi-sdk-ext.x509-utils": "workspace:*", "@sphereon/ssi-sdk-ext.identifier-resolution": "workspace:*", "@sphereon/ssi-sdk-ext.key-manager": "workspace:*", + "@stablelib/random": "^1.0.2", + "jose": "^5.9.3", "uint8arrays": "^3.1.1", "jwt-decode": "^4.0.0", "@veramo/core": "4.2.0", diff --git a/packages/jwt-service/src/agent/JwtService.ts b/packages/jwt-service/src/agent/JwtService.ts index d9bdea40..ecb84fe0 100644 --- a/packages/jwt-service/src/agent/JwtService.ts +++ b/packages/jwt-service/src/agent/JwtService.ts @@ -1,55 +1,104 @@ -import { IAgentPlugin } from '@veramo/core' +import {IAgentPlugin} from '@veramo/core' import { - createJwsCompact, - CreateJwsCompactArgs, - CreateJwsFlattenedArgs, - CreateJwsJsonArgs, - createJwsJsonFlattened, - createJwsJsonGeneral, - IJwsValidationResult, - IJwtService, - IRequiredContext, - JwsCompactResult, - JwsJsonFlattened, - JwsJsonGeneral, - PreparedJwsObject, - prepareJwsObject, - schema, - verifyJws, - VerifyJwsArgs, + createJwsCompact, + CreateJwsCompactArgs, + CreateJwsFlattenedArgs, + CreateJwsJsonArgs, + createJwsJsonFlattened, + createJwsJsonGeneral, + DecryptJweCompactJwtArgs, + EncryptJweCompactJwtArgs, + IJwsValidationResult, + IJwtService, + IRequiredContext, + jweAlg, + jweEnc, + JwsJsonFlattened, + JwsJsonGeneral, + JwtCompactResult, + JwtLogger, + PreparedJwsObject, + prepareJwsObject, + schema, + verifyJws, + VerifyJwsArgs, } from '..' +import {CompactJwtEncrypter} from "../functions/JWE"; /** * @public */ export class JwtService implements IAgentPlugin { - readonly schema = schema.IJwtService - readonly methods: IJwtService = { - jwtPrepareJws: this.jwtPrepareJws.bind(this), - jwtCreateJwsJsonGeneralSignature: this.jwtCreateJwsJsonGeneralSignature.bind(this), - jwtCreateJwsJsonFlattenedSignature: this.jwtCreateJwsJsonFlattenedSignature.bind(this), - jwtCreateJwsCompactSignature: this.jwtCreateJwsCompactSignature.bind(this), - jwtVerifyJwsSignature: this.jwtVerifyJwsSignature.bind(this), - } - - private async jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise { - return await prepareJwsObject(args, context) - } - - private async jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise { - return await createJwsJsonGeneral(args, context) - } - - private async jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise { - return await createJwsJsonFlattened(args, context) - } - - private async jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise { - // We wrap it in a json object for remote REST calls - return { jwt: await createJwsCompact(args, context) } - } - - private async jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise { - return await verifyJws(args, context) - } + readonly schema = schema.IJwtService + readonly methods: IJwtService = { + jwtPrepareJws: this.jwtPrepareJws.bind(this), + jwtCreateJwsJsonGeneralSignature: this.jwtCreateJwsJsonGeneralSignature.bind(this), + jwtCreateJwsJsonFlattenedSignature: this.jwtCreateJwsJsonFlattenedSignature.bind(this), + jwtCreateJwsCompactSignature: this.jwtCreateJwsCompactSignature.bind(this), + jwtVerifyJwsSignature: this.jwtVerifyJwsSignature.bind(this), + jwtEncryptJweCompactJwt: this.jwtEncryptJweCompactJwt.bind(this), + jwtDecryptJweCompactJwt: this.jwtDecryptJweCompactJwt.bind(this) + } + + private async jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise { + return await prepareJwsObject(args, context) + } + + private async jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise { + return await createJwsJsonGeneral(args, context) + } + + private async jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise { + return await createJwsJsonFlattened(args, context) + } + + private async jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise { + // We wrap it in a json object for remote REST calls + return {jwt: await createJwsCompact(args, context)} + } + + private async jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise { + return await verifyJws(args, context) + } + + private async jwtEncryptJweCompactJwt(args: EncryptJweCompactJwtArgs, context: IRequiredContext): Promise { + const {payload, protectedHeader = {alg: args.alg, enc: args.enc}, recipientKey, keyManagementParams, issuer, expirationTime, audience} = args + + const alg = jweAlg(args.alg) ?? jweAlg(protectedHeader.alg) ?? 'ECDH-ES' + const enc = jweEnc(args.enc) ?? jweEnc(protectedHeader.enc) ?? 'A256GCM' + const encJwks = recipientKey.jwks.length === 1 ? [recipientKey.jwks[0]] : recipientKey.jwks.filter(jwk => (jwk.kid && (jwk.kid === jwk.jwk.kid || jwk.kid === jwk.jwkThumbprint)) || jwk.jwk.use === 'enc') + if (encJwks.length === 0) { + return Promise.reject(Error(`No public JWK found that can be used to encrypt against`)) + } + const jwkInfo = encJwks[0] + if (encJwks.length > 0) { + JwtLogger.warning(`More than one JWK with 'enc' usage found. Selected the first one as no 'kid' was provided`, encJwks) + } + if (jwkInfo.jwk.kty?.startsWith('EC') !== true || !alg.startsWith('ECDH')) { + return Promise.reject(Error(`Currently only ECDH-ES is supported for encryption. JWK alg ${jwkInfo.jwk.kty}, header alg ${alg}`)) // TODO: Probably we support way more already + } + const {apu, apv} = {...keyManagementParams} + + const pubKey = await crypto.subtle.importKey('jwk', jwkInfo.jwk, { + name: 'ECDH', + namedCurve: 'P-256', + }, true, []) + const encrypter = new CompactJwtEncrypter({ + enc, + alg, + keyManagementParams: {apu, apv}, + key: pubKey, + issuer, + expirationTime, + audience + }) + + const jwe = await encrypter.encryptCompactJWT(payload, {}) + return {jwt: jwe} + } + + private async jwtDecryptJweCompactJwt(args: DecryptJweCompactJwtArgs, context: IRequiredContext): Promise { + + return {jwt: "FIXME"} + } } diff --git a/packages/jwt-service/src/functions/JWE.ts b/packages/jwt-service/src/functions/JWE.ts new file mode 100644 index 00000000..bcf686e4 --- /dev/null +++ b/packages/jwt-service/src/functions/JWE.ts @@ -0,0 +1,358 @@ +import {defaultRandomSource, randomBytes, RandomSource} from '@stablelib/random' +import {base64ToBytes, bytesToBase64url, decodeBase64url} from "@veramo/utils"; +import * as jose from "jose"; +import {JWEKeyManagementHeaderParameters, JWTDecryptOptions} from "jose"; +import type {KeyLike} from "jose/dist/types/types"; +import * as u8a from 'uint8arrays' +import { + JweAlg, + JweAlgs, + JweEnc, + JweEncs, + JweHeader, + JweJsonGeneral, + JweProtectedHeader, + JweRecipient, + JweRecipientUnprotectedHeader, + JwsPayload +} from "../types/IJwtService"; + + +export interface EncryptionResult { + ciphertext: Uint8Array + tag: Uint8Array + iv: Uint8Array + protectedHeader?: string + recipients?: JweRecipient[] + cek?: Uint8Array +} + + +export const generateContentEncryptionKey = async ({alg, randomSource = defaultRandomSource}: { + alg: JweEnc, + randomSource?: RandomSource +}): Promise => { + let length: number + switch (alg) { + case "A128GCM": + length = 16 + break + case "A192GCM": + length = 24 + break + case "A128CBC-HS256": + case "A256GCM": + length = 32 + break + case "A192CBC-HS384": + length = 48 + break + case "A256CBC-HS512": + length = 64 + break + default: + length = 32 + } + return randomBytes(length, randomSource) +} + +/* +export const generateContentEncryptionKeyfdsdf = async ({type = 'Secp256r1', ...rest}: { + type?: Extract, + kms?: string +}, context: IAgentContext): Promise => { + + const kms = rest.kms ?? await context.agent.keyManagerGetDefaultKeyManagementSystem() + const key = await context.agent.keyManagerCreate({kms, type, opts: {ephemeral: true}}) + const jwk = toJwkFromKey(key, {use: JwkKeyUse.Encryption, noKidThumbprint: true}) +} +*/ +export interface JwtEncrypter { + alg: string + enc: string + encrypt: (payload: JwsPayload, protectedHeader: JweProtectedHeader, aad?: Uint8Array) => Promise + encryptCek?: (cek: Uint8Array) => Promise +} + + +export interface JweEncrypter { + alg: string + enc: string + encrypt: (payload: Uint8Array, protectedHeader: JweProtectedHeader, aad?: Uint8Array) => Promise + encryptCek?: (cek: Uint8Array) => Promise +} + +export interface JweDecrypter { + alg: string + enc: string + decrypt: (sealed: Uint8Array, iv: Uint8Array, aad?: Uint8Array, recipient?: JweRecipient) => Promise +} + +function jweAssertValid(jwe: JweJsonGeneral) { + if (!(jwe.protected && jwe.iv && jwe.ciphertext && jwe.tag)) { + throw Error('JWE is missing properties: protected, iv, ciphertext and/or tag') + } + if (jwe.recipients) { + jwe.recipients.map((recipient: JweRecipient) => { + if (!(recipient.header && recipient.encrypted_key)) { + throw Error('Malformed JWE recipients; no header and encrypted key present') + } + }) + } +} + +function jweEncode({ciphertext, tag, iv, protectedHeader, recipients, aad, unprotected}: EncryptionResult & { + aad?: Uint8Array, + unprotected?: JweHeader +}): JweJsonGeneral { + if (!recipients || recipients.length === 0) { + throw Error(`No recipient found`) + } + return { + ...(unprotected && {unprotected}), + protected: protectedHeader, + iv: bytesToBase64url(iv), + ciphertext: bytesToBase64url(ciphertext), + ...(tag && {tag: bytesToBase64url(tag)}), + ...(aad && {aad: bytesToBase64url(aad)}), + recipients + } satisfies JweJsonGeneral +} + +export class CompactJwtEncrypter implements JweEncrypter { + private _alg: JweAlg | undefined; + private _enc: JweEnc | undefined; + private _keyManagementParams: JWEKeyManagementHeaderParameters | undefined + private recipientKey: Uint8Array | jose.KeyLike //,EphemeralPublicKey | BaseJWK; + private expirationTime + private issuer: string | undefined + private audience: string | string[] | undefined + + constructor(args: { + key: Uint8Array | jose.KeyLike /*EphemeralPublicKey | BaseJWK*/, + alg?: JweAlg, + enc?: JweEnc, + keyManagementParams?: JWEKeyManagementHeaderParameters, + expirationTime?: number | string | Date + issuer?: string + audience?: string | string[] + }) { + if (args?.alg) { + this._alg = args.alg + } + if (args?.enc) { + this._enc = args.enc + } + this._keyManagementParams = args.keyManagementParams + this.recipientKey = args.key + this.expirationTime = args.expirationTime ?? '10 minutes' + this.issuer = args.issuer + this.audience = args.audience + } + + get enc(): string { + if (!this._enc) { + throw Error(`enc not set`) + } + return this._enc; + } + + set enc(value: JweEnc | string) { + // @ts-ignore + if (!JweEncs.includes(value)) { + throw Error(`invalid JWE enc value ${value}`) + } + this._enc = value as JweEnc; + } + + get alg(): string { + if (!this._alg) { + throw Error(`alg not set`) + } + return this._alg; + } + + set alg(value: JweAlg | string) { + // @ts-ignore + if (!JweAlgs.includes(value)) { + throw Error(`invalid JWE alg value ${value}`) + } + this._alg = value as JweAlg; + } + + async encryptCompactJWT( + payload: JwsPayload, + jweProtectedHeader: JweProtectedHeader, + aad?: Uint8Array | undefined + ): Promise { + const protectedHeader = { + ...jweProtectedHeader, + alg: jweProtectedHeader.alg ?? this._alg, + enc: jweProtectedHeader.enc ?? this._enc + } + if (!protectedHeader.alg || !protectedHeader.enc) { + return Promise.reject(Error(`no 'alg' or 'enc' value set for the protected JWE header!`)) + } + this.enc = protectedHeader.enc + this.alg = protectedHeader.alg + if (payload.exp) { + this.expirationTime = payload.exp + } + if (payload.iss) { + this.issuer = payload.iss + } + if (payload.aud) { + this.audience = payload.aud + } + const encrypt = new jose.EncryptJWT(payload).setProtectedHeader({ + ...protectedHeader, + alg: this.alg, + enc: this.enc + }) + if (this._alg!.startsWith('ECDH')) { + if (!this._keyManagementParams) { + return Promise.reject(Error(`ECDH requires key management params`)) + } + encrypt.setKeyManagementParameters(this._keyManagementParams!) + } + // We always set the expiration time for encrypted JWTs (values are set above) + encrypt.setExpirationTime(this.expirationTime) + if (this.issuer) { + encrypt.setIssuer(this.issuer) + } + if (this.audience) { + encrypt.setAudience(this.audience) + } + return await encrypt.encrypt(this.recipientKey) + } + + public static async decryptCompactJWT(jwt: string, key: KeyLike | Uint8Array, options?: JWTDecryptOptions) { + return await jose.jwtDecrypt(jwt, key, options) + } + + async encrypt( + payload: Uint8Array, + jweProtectedHeader: JweProtectedHeader, + aad?: Uint8Array | undefined + ): Promise { + const jwt = await this.encryptCompactJWT(JSON.parse(u8a.toString(payload)), jweProtectedHeader, aad) + const [protectedHeader, encryptedKey, ivB64, payloadB64, tagB64,] = jwt.split('.') + //[jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join('.'); + console.log(`FIXME: TO EncryptionResult`) + + return { + protectedHeader, + tag: base64ToBytes(tagB64), + ciphertext: base64ToBytes(payloadB64), + iv: base64ToBytes(ivB64), + recipients: [ + + { + //fixme + // header: protectedHeader, + ...(encryptedKey && { encrypted_key: encryptedKey}) + + } + ] + } + + } + + // encryptCek?: ((cek: Uint8Array) => Promise) | undefined; + +} + +export async function createJwe( + cleartext: Uint8Array, + encrypters: JweEncrypter[], + protectedHeader: JweProtectedHeader, + aad?: Uint8Array +): Promise { + if (encrypters.length === 0) { + throw Error('JWE needs at least 1 encryptor') + } + if (encrypters.find(enc => enc.alg === 'dir' || enc.alg === 'ECDH-ES')) { + if (encrypters.length !== 1) { + throw Error(`JWE can only do "dir" or "ECDH-ES" encryption with one key. ${encrypters.length} supplied`) + } + const encryptionResult = await encrypters[0].encrypt(cleartext, protectedHeader, aad) + return jweEncode({...encryptionResult, aad}) + } else { + const tmpEnc = encrypters[0].enc + if (!encrypters.reduce((acc, encrypter) => acc && encrypter.enc === tmpEnc, true)) { + throw new Error('invalid_argument: Incompatible encrypters passed') + } + let cek: Uint8Array | undefined = undefined + let jwe: JweJsonGeneral | undefined = undefined + for (const encrypter of encrypters) { + if (!cek) { + const encryptionResult = await encrypter.encrypt(cleartext, protectedHeader, aad) + cek = encryptionResult.cek + jwe = jweEncode({...encryptionResult, aad}) + } else { + const recipient = await encrypter.encryptCek?.(cek) + if (recipient) { + jwe?.recipients?.push(recipient) + } + } + } + if (!jwe) { + throw Error(`No JWE constructed`) + } + return jwe + } +} + +/** + * Merges all headers, so we get a unified header. + * + * @param protectedHeader + * @param unprotectedHeader + * @param recipientUnprotectedHeader + */ +export function jweMergeHeaders({protectedHeader, unprotectedHeader, recipientUnprotectedHeader}: { + protectedHeader?: JweProtectedHeader, + unprotectedHeader?: JweHeader, + recipientUnprotectedHeader?: JweRecipientUnprotectedHeader +}): JweHeader { + // TODO: Check that all headers/params are disjoint! + const header = {...protectedHeader, ...unprotectedHeader, ...recipientUnprotectedHeader} + + if (!header.alg || !header.enc) { + throw Error(`Either 'alg' or 'enc' are missing from the headers`) + } + return header as JweHeader +} + +export async function decryptJwe(jwe: JweJsonGeneral, decrypter: JweDecrypter): Promise { + jweAssertValid(jwe) + const protectedHeader: JweProtectedHeader = JSON.parse(decodeBase64url(jwe.protected)) + if (protectedHeader?.enc !== decrypter.enc) { + return Promise.reject(Error(`Decrypter enc '${decrypter.enc}' does not support header enc '${protectedHeader.enc}'`)) + } else if (!jwe.tag) { + return Promise.reject(Error(`Decrypter enc '${decrypter.enc}' does not support header enc '${protectedHeader.enc}'`)) + } + const sealed = toWebCryptoCiphertext(jwe.ciphertext, jwe.tag) + const aad = u8a.fromString(jwe.aad ? `${jwe.protected}.${jwe.aad}` : jwe.protected) + let cleartext = null + if (protectedHeader.alg === 'dir' && decrypter.alg === 'dir') { + cleartext = await decrypter.decrypt(sealed, base64ToBytes(jwe.iv), aad) + } else if (!jwe.recipients || jwe.recipients.length === 0) { + throw Error('missing recipients for JWE') + } else { + for (let i = 0; !cleartext && i < jwe.recipients.length; i++) { + const recipient: JweRecipient = jwe.recipients[i] + recipient.header = {...recipient.header, ...protectedHeader} as JweRecipientUnprotectedHeader + if (recipient.header.alg === decrypter.alg) { + cleartext = await decrypter.decrypt(sealed, base64ToBytes(jwe.iv), aad, recipient) + } + } + } + if (cleartext === null) throw new Error('failure: Failed to decrypt') + return cleartext +} + + +export function toWebCryptoCiphertext(ciphertext: string, tag: string): Uint8Array { + return u8a.concat([base64ToBytes(ciphertext), base64ToBytes(tag)]) +} diff --git a/packages/jwt-service/src/functions/index.ts b/packages/jwt-service/src/functions/index.ts index d3bde578..ddcaf0f5 100644 --- a/packages/jwt-service/src/functions/index.ts +++ b/packages/jwt-service/src/functions/index.ts @@ -33,13 +33,13 @@ import { JwsJsonGeneral, JwsJsonGeneralWithIdentifiers, JwsJsonSignature, JwsJsonSignatureWithIdentifier, - JwtHeader, - JwtPayload, + JwsHeader, + JwsPayload, PreparedJwsObject, - VerifyJwsArgs, + VerifyJwsArgs, JweHeader, } from '../types/IJwtService' -const payloadToBytes = (payload: string | JwtPayload | Uint8Array): Uint8Array => { +const payloadToBytes = (payload: string | JwsPayload | Uint8Array): Uint8Array => { const isBytes = payload instanceof Uint8Array const isString = typeof payload === 'string' return isBytes ? payload : isString ? u8a.fromString(payload, 'base64url') : u8a.fromString(JSON.stringify(payload), 'utf-8') @@ -50,7 +50,7 @@ export const prepareJwsObject = async (args: CreateJwsJsonArgs, context: IRequir const { noIdentifierInHeader = false } = issuer const identifier = await ensureManagedIdentifierResult(issuer, context) - await checkAndUpdateJwtHeader({ mode, identifier, noIdentifierInHeader, header: protectedHeader }, context) + await checkAndUpdateJwsHeader({ mode, identifier, noIdentifierInHeader, header: protectedHeader }, context) const isBytes = payload instanceof Uint8Array const isString = typeof payload === 'string' if (!isBytes && !isString) { @@ -138,7 +138,7 @@ export const createJwsJsonGeneral = async (args: CreateJwsJsonArgs, context: IRe * @param context */ -export const checkAndUpdateJwtHeader = async ( +export const checkAndUpdateJwsHeader = async ( { mode = 'auto', identifier, @@ -148,7 +148,7 @@ export const checkAndUpdateJwtHeader = async ( mode?: JwsIdentifierMode identifier: ManagedIdentifierResult noIdentifierInHeader?: boolean - header: JwtHeader + header: JwsHeader }, context: IRequiredContext ) => { @@ -179,7 +179,7 @@ const checkAndUpdateX5cHeader = async ( identifier, noIdentifierInHeader = false, }: { - header: JwtHeader + header: JwsHeader | JweHeader identifier: ManagedIdentifierResult noIdentifierInHeader?: boolean }, @@ -208,7 +208,7 @@ const checkAndUpdateDidHeader = async ( identifier, noIdentifierInHeader = false, }: { - header: JwtHeader + header: JwsHeader | JweHeader identifier: ManagedIdentifierResult noIdentifierInHeader?: boolean }, @@ -237,7 +237,7 @@ const checkAndUpdateJwkHeader = async ( identifier, noIdentifierInHeader = false, }: { - header: JwtHeader + header: JwsHeader | JweHeader identifier: ManagedIdentifierResult noIdentifierInHeader?: boolean }, @@ -246,7 +246,7 @@ const checkAndUpdateJwkHeader = async ( const { jwk } = header if (jwk) { // let's resolve the provided x5c to be sure - const jwkIdentifier = await context.agent.identifierManagedGetByJwk({ identifier: jwk }) + const jwkIdentifier = await context.agent.identifierManagedGetByJwk({ identifier: jwk as JWK }) if (jwkIdentifier.kmsKeyRef !== identifier.kmsKeyRef) { return Promise.reject(Error(`A jwk header was present, but its value did not match the provided signing jwk or kid!`)) } @@ -265,7 +265,7 @@ const checkAndUpdateKidHeader = async ( identifier, noIdentifierInHeader = false, }: { - header: JwtHeader + header: JwsHeader | JweHeader identifier: ManagedIdentifierResult noIdentifierInHeader?: boolean }, @@ -376,7 +376,7 @@ export const toJwsJsonGeneral = async ({ jws }: { jws: Jws }, context: IAgentCon } async function resolveExternalIdentifierFromJwsHeader( - protectedHeader: JwtHeader, + protectedHeader: JwsHeader, context: IAgentContext, args: { jws: Jws @@ -422,7 +422,7 @@ export const toJwsJsonGeneralWithIdentifiers = async ( const jws = await toJwsJsonGeneral(args, context) const signatures = (await Promise.all( jws.signatures.map(async (signature) => { - const protectedHeader: JwtHeader = decodeJoseBlob(signature.protected) + const protectedHeader: JwsHeader = decodeJoseBlob(signature.protected) const identifier = args.jwk ? await resolveExternalJwkIdentifier({ identifier: args.jwk }, context) : await resolveExternalIdentifierFromJwsHeader(protectedHeader, context, args) @@ -432,6 +432,6 @@ export const toJwsJsonGeneralWithIdentifiers = async ( return undefined }) )).filter(signature => signature !== undefined ) as JwsJsonSignatureWithIdentifier[] - + return { payload: jws.payload, signatures } } diff --git a/packages/jwt-service/src/index.ts b/packages/jwt-service/src/index.ts index 63140ba8..86d1179e 100644 --- a/packages/jwt-service/src/index.ts +++ b/packages/jwt-service/src/index.ts @@ -1,8 +1,12 @@ +import {Loggers} from "@sphereon/ssi-types"; + /** * @internal */ const schema = require('../plugin.schema.json') export { schema } + +export const JwtLogger = Loggers.DEFAULT.get('sphereon:sdk:jwt') /** * @public */ diff --git a/packages/jwt-service/src/types/IJwtService.ts b/packages/jwt-service/src/types/IJwtService.ts index 60e8d52c..ac0929f9 100644 --- a/packages/jwt-service/src/types/IJwtService.ts +++ b/packages/jwt-service/src/types/IJwtService.ts @@ -1,167 +1,330 @@ import { - ExternalIdentifierDidOpts, - ExternalIdentifierResult, - ExternalIdentifierX5cOpts, - IIdentifierResolution, - ManagedIdentifierOptsOrResult, - ManagedIdentifierResult, + ExternalIdentifierDidOpts, + ExternalIdentifierResult, + ExternalIdentifierX5cOpts, + IIdentifierResolution, + ManagedIdentifierOptsOrResult, + ManagedIdentifierResult, } from '@sphereon/ssi-sdk-ext.identifier-resolution' -import { ClientIdScheme } from '@sphereon/ssi-sdk-ext.x509-utils' -import { IValidationResult, JoseSignatureAlgorithm, JoseSignatureAlgorithmString, JWK } from '@sphereon/ssi-types' -import { IAgentContext, IKeyManager, IPluginMethodMap } from '@veramo/core' +import {ClientIdScheme} from '@sphereon/ssi-sdk-ext.x509-utils' +import { + BaseJWK, + IValidationResult, + JoseSignatureAlgorithm, + JoseSignatureAlgorithmString, + JWK +} from '@sphereon/ssi-types' +import {IAgentContext, IKeyManager, IPluginMethodMap} from '@veramo/core' +import {JWEKeyManagementHeaderParameters} from "jose"; export type IRequiredContext = IAgentContext // could we still interop with Veramo? export const jwtServiceContextMethods: Array = [ - 'jwtPrepareJws', - 'jwtCreateJwsJsonGeneralSignature', - 'jwtCreateJwsJsonFlattenedSignature', - 'jwtCreateJwsCompactSignature', - 'jwtVerifyJwsSignature', + 'jwtPrepareJws', + 'jwtCreateJwsJsonGeneralSignature', + 'jwtCreateJwsJsonFlattenedSignature', + 'jwtCreateJwsCompactSignature', + 'jwtVerifyJwsSignature', + 'jwtEncryptJweCompactJwt', + 'jwtDecryptJweCompactJwt' ] export interface IJwtService extends IPluginMethodMap { - jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise + jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise + + jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise - jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise + jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise - jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise + jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise - jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise + jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise - jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise + jwtEncryptJweCompactJwt(args: EncryptJweCompactJwtArgs, context: IRequiredContext): Promise - // TODO: JWE/encryption + jwtDecryptJweCompactJwt(args: DecryptJweCompactJwtArgs, context: IRequiredContext): Promise + + // TODO: JWE/encryption general methods } export type IJwsValidationResult = IValidationResult & { - jws: JwsJsonGeneralWithIdentifiers // We always translate to general as that is the most flexible format allowing multiple sigs + jws: JwsJsonGeneralWithIdentifiers // We always translate to general as that is the most flexible format allowing multiple sigs } export interface PreparedJws { - protectedHeader: JwtHeader - payload: Uint8Array - unprotectedHeader?: JwtHeader // only for jws json and also then optional - existingSignatures?: Array // only for jws json and also then optional + protectedHeader: JwsHeader + payload: Uint8Array + unprotectedHeader?: JwsHeader // only for jws json and also then optional + existingSignatures?: Array // only for jws json and also then optional } export interface JwsJsonSignature { - protected: string - header?: JwtHeader - signature: string + protected: string + header?: JwsHeader + signature: string } +/** + * The JWK representation of an ephemeral public key. + * See https://www.rfc-editor.org/rfc/rfc7518.html#section-6 + */ +// todo split into separate objects +export type EphemeralPublicKey = Omit + +// export function isEcJWK(v) + +export interface JweHeader extends Omit { + alg: string, + enc: string, + jku?: string, + jwk?: BaseJWK, + epk?: EphemeralPublicKey, + x5u?: string + x5c?: string[] + x5t?: string, + cty?: string, + crit?: string[] + + [k: string]: any + +} + + +export interface JweRecipientUnprotectedHeader { + alg: string + iv: string + tag: string + epk?: EphemeralPublicKey + kid?: string + apv?: string + apu?: string +} + +export interface JweProtectedHeader extends Partial { + zip?: 'DEF' | string +} + + export type Jws = JwsCompact | JwsJsonFlattened | JwsJsonGeneral export type JwsCompact = string export interface JwsJsonFlattened { - payload: string - protected: string - header?: JwtHeader - signature: string + payload: string + protected: string + header?: JwsHeader + signature: string } export interface JwsJsonGeneral { - payload: string - signatures: Array + payload: string + signatures: Array } export interface JwsJsonGeneralWithIdentifiers extends JwsJsonGeneral { - signatures: Array + signatures: Array } export interface JwsJsonSignatureWithIdentifier extends JwsJsonSignature { - identifier: ExternalIdentifierResult + identifier: ExternalIdentifierResult +} + + +export type Jwe = JweCompact | JweJsonFlattened | JweJsonGeneral +export type JweCompact = string + +export interface JweJsonFlattened { + protected: string + unprotected: JweHeader + header: JweHeader | JweRecipientUnprotectedHeader + encrypted_key?: string + aad?: string + iv: string + ciphertext: string + tag?: string +} + + +export interface JweRecipient { + header?: JweRecipientUnprotectedHeader + encrypted_key?: string +} + +export interface JweJsonGeneral { + protected: string + unprotected?: JweHeader + recipients: Array + aad?: string + iv: string + ciphertext: string + tag?: string } + export interface PreparedJwsObject { - jws: PreparedJws - b64: { payload: string; protectedHeader: string } // header is always json, as it can only be used in JwsJson - identifier: ManagedIdentifierResult + jws: PreparedJws + b64: { payload: string; protectedHeader: string } // header is always json, as it can only be used in JwsJson + identifier: ManagedIdentifierResult } export interface BaseJwtHeader { - typ?: string - alg?: string - kid?: string + typ?: string + alg?: string + kid?: string } export interface BaseJwtPayload { - iss?: string - sub?: string - aud?: string[] | string - exp?: number - nbf?: number - iat?: number - jti?: string + iss?: string + sub?: string + aud?: string[] | string + exp?: number + nbf?: number + iat?: number + jti?: string } -export interface JwtHeader extends BaseJwtHeader { - kid?: string - jwk?: JWK - x5c?: string[] +export interface JwsHeader extends BaseJwtHeader { + kid?: string + jwk?: JWK + x5c?: string[] - [key: string]: unknown + [key: string]: unknown } -export interface JwtPayload extends BaseJwtPayload { - [key: string]: unknown +export interface JwsPayload extends BaseJwtPayload { + [key: string]: unknown } export interface JwsHeaderOpts { - alg: JoseSignatureAlgorithm | JoseSignatureAlgorithmString + alg: JoseSignatureAlgorithm | JoseSignatureAlgorithmString } export type JwsIdentifierMode = 'x5c' | 'kid' | 'jwk' | 'did' | 'auto' +export type EncryptJweCompactJwtArgs = { + payload: JwsPayload, + protectedHeader?: JweProtectedHeader | undefined, + aad?: Uint8Array | undefined + recipientKey: ExternalIdentifierResult & { kid?: string} + alg?: JweAlg + enc?: JweEnc + keyManagementParams?: JWEKeyManagementHeaderParameters + expirationTime?: number | string | Date + issuer?: string + audience?: string | string[] +} + +export type DecryptJweCompactJwtArgs = { + jwe: JweCompact + idOpts: ManagedIdentifierOptsOrResult +} + export type CreateJwsArgs = { - mode?: JwsIdentifierMode - issuer: ManagedIdentifierOptsOrResult & { - noIssPayloadUpdate?: boolean - noIdentifierInHeader?: boolean - } - clientId?: string - clientIdScheme?: ClientIdScheme | 'did' | string - protectedHeader: JwtHeader - payload: JwtPayload | Uint8Array | string + mode?: JwsIdentifierMode + issuer: ManagedIdentifierOptsOrResult & { + noIssPayloadUpdate?: boolean + noIdentifierInHeader?: boolean + } + clientId?: string + clientIdScheme?: ClientIdScheme | 'did' | string + protectedHeader: JwsHeader + payload: JwsPayload | Uint8Array | string } + +export type CreateJweArgs = { + mode?: JwsIdentifierMode + issuer: ManagedIdentifierOptsOrResult & { + noIssPayloadUpdate?: boolean + noIdentifierInHeader?: boolean + } + protectedHeader: JweProtectedHeader + encryptedKey: string | EphemeralPublicKey // In case it is a string it is already encrypted; otherwise encrypt //TODO ?? + iv: string + ciphertext: string + tag: string +} export type CreateJwsCompactArgs = CreateJwsArgs export type CreateJwsFlattenedArgs = Exclude + + + export type VerifyJwsArgs = { - jws: Jws - jwk?: JWK // Jwk will be resolved from jws, but you can also provide one - opts?: { x5c?: Omit; did?: Omit } + jws: Jws + jwk?: JWK // Jwk will be resolved from jws, but you can also provide one + opts?: { x5c?: Omit; did?: Omit } } /** * @public */ export type CreateJwsJsonArgs = CreateJwsArgs & { - unprotectedHeader?: JwtHeader // only for jws json - existingSignatures?: Array // Only for jws json + unprotectedHeader?: JwsHeader // only for jws json + existingSignatures?: Array // Only for jws json +} + +export type CreateJweJsonArgs = CreateJweArgs & { + unprotectedHeader?: JweHeader } /** * @public */ -export interface JwsCompactResult { - jwt: JwsCompact +export interface JwtCompactResult { + jwt: JwsCompact | JweCompact } export function isJwsCompact(jws: Jws): jws is JwsCompact { - return typeof jws === 'string' && jws.split('~')[0].match(COMPACT_JWS_REGEX) !== null + return typeof jws === 'string' && jws.split('~')[0].match(COMPACT_JWS_REGEX) !== null +} + +export function isJweCompact(jwe: Jwe): jwe is JweCompact { + return typeof jwe === 'string' && jwe.split('~')[0].match(COMPACT_JWE_REGEX) !== null } + export function isJwsJsonFlattened(jws: Jws): jws is JwsJsonFlattened { - return typeof jws === 'object' && 'signature' in jws && 'protected' in jws + return typeof jws === 'object' && 'signature' in jws && 'protected' in jws && !('ciphertext' in jws) } export function isJwsJsonGeneral(jws: Jws): jws is JwsJsonGeneral { - return typeof jws === 'object' && 'signatures' in jws + return typeof jws === 'object' && 'signatures' in jws && !('ciphertext' in jws) +} + + +export function isJweJsonFlattened(jwe: Jwe): jwe is JweJsonFlattened { + return typeof jwe === 'object' && 'signature' in jwe && 'ciphertext' in jwe && !('payload' in jwe) +} + +export function isJweJsonGeneral(jwe: Jwe): jwe is JweJsonGeneral { + return typeof jwe === 'object' && 'signatures' in jwe && 'ciphertext' in jwe && !('payload' in jwe) +} + + +export function isJwsHeader(header: BaseJwtHeader & Record): header is JwsHeader { + return header && !isJweHeader(header) +} + +export function isJweHeader(header: BaseJwtHeader & Record): header is JweHeader { + return ('enc' in header && header.enc && jweEnc(header.enc)) || (header.alg && jweAlg(header.alg)) } export const COMPACT_JWS_REGEX = /^([a-zA-Z0-9_=-]+).([a-zA-Z0-9_=-]+)?.([a-zA-Z0-9_=-]+)?$/ +export const COMPACT_JWE_REGEX = /^([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)?$/ + +export const JweAlgs = ['RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256', 'A128KW', 'A192KW', 'A256KW', 'dir', 'ECDH-ES'/*interop value*/, 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW', 'A128GCMKW', 'A192GCMKW', 'A256GCMKW', 'PBES2-HS256+A128KW', 'PBES2-HS384+A192KW', 'PBES2-HS512+A256KW'] as const; +export type JweAlg = typeof JweAlgs[number] +export function jweAlg(alg?: string | JweAlg): JweAlg | undefined { + return JweAlgs.find((supportedVal) => supportedVal === alg); +} + + +export const JweEncs = ['A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM'/*interop value*/] as const +export type JweEnc = typeof JweEncs[number] + +export function jweEnc(alg?: string | JweEnc): JweEnc | undefined { + return JweEncs.find((supportedVal) => supportedVal === alg); +} + diff --git a/packages/key-manager/src/agent/SphereonKeyManager.ts b/packages/key-manager/src/agent/SphereonKeyManager.ts index 3e1abaf5..a86318fc 100644 --- a/packages/key-manager/src/agent/SphereonKeyManager.ts +++ b/packages/key-manager/src/agent/SphereonKeyManager.ts @@ -51,7 +51,7 @@ export class SphereonKeyManager extends VeramoKeyManager { } override async keyManagerCreate(args: ISphereonKeyManagerCreateArgs): Promise { - const kms = this.getKmsByName(args.kms) + const kms = this.getKmsByName(args.kms ?? this.defaultKms) const meta: KeyMetadata = { ...args.meta, ...(args.opts && { opts: args.opts }) } if (hasKeyOptions(meta) && meta.opts?.ephemeral && !meta.opts.expiration?.removalDate) { // Make sure we set a delete date on an ephemeral key @@ -61,7 +61,7 @@ export class SphereonKeyManager extends VeramoKeyManager { } } const partialKey = await kms.createKey({ type: args.type, meta }) - const key: IKey = { ...partialKey, kms: args.kms } + const key: IKey = { ...partialKey, kms: args.kms ?? this.defaultKms } key.meta = { ...meta, ...key.meta } key.meta.jwkThumbprint = key.meta.jwkThumbprint ?? calculateJwkThumbprintForKey({ key }) diff --git a/packages/key-manager/src/types/ISphereonKeyManager.ts b/packages/key-manager/src/types/ISphereonKeyManager.ts index 40ce8996..aa2dd6c1 100644 --- a/packages/key-manager/src/types/ISphereonKeyManager.ts +++ b/packages/key-manager/src/types/ISphereonKeyManager.ts @@ -60,7 +60,7 @@ export interface ISphereonKeyManagerCreateArgs { /** * Key Management System */ - kms: string + kms?: string /** * Key options diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4994ada..3d70fb30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,7 +219,7 @@ importers: version: link:../key-utils '@transmute/did-key-bls12381': specifier: 0.3.0-unstable.10 - version: 0.3.0-unstable.10 + version: 0.3.0-unstable.10(expo@51.0.26)(react-native@0.74.5) '@veramo/core': specifier: 4.2.0 version: 4.2.0(patch_hash=c5oempznsz4br5w3tcuk2i2mau) @@ -560,6 +560,9 @@ importers: '@sphereon/ssi-types': specifier: 0.29.1-unstable.161 version: 0.29.1-unstable.161 + '@stablelib/random': + specifier: ^1.0.2 + version: 1.0.2 '@veramo/core': specifier: 4.2.0 version: 4.2.0(patch_hash=c5oempznsz4br5w3tcuk2i2mau) @@ -569,6 +572,9 @@ importers: debug: specifier: ^4.3.4 version: 4.3.6 + jose: + specifier: ^5.9.3 + version: 5.9.3 jwt-decode: specifier: ^4.0.0 version: 4.0.0 @@ -5364,7 +5370,7 @@ packages: /@transmute/did-context@0.6.1-unstable.37: resolution: {integrity: sha512-p/QnG3QKS4218hjIDgdvJOFATCXsAnZKgy4egqRrJLlo3Y6OaDBg7cA73dixOwUPoEKob0K6rLIGcsCI/L1acw==} - /@transmute/did-key-bls12381@0.3.0-unstable.10: + /@transmute/did-key-bls12381@0.3.0-unstable.10(expo@51.0.26)(react-native@0.74.5): resolution: {integrity: sha512-ExSADdvDxrYeCx8RsKXZGMjJmHrOJ9vyYtziZUaJ97K/sn1uVlvIOTp9V4xHa6j9cT1wTzSqJ325euwGFeK+WQ==} engines: {node: '>=14'} dependencies: @@ -11589,6 +11595,10 @@ packages: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} dev: true + /jose@5.9.3: + resolution: {integrity: sha512-egLIoYSpcd+QUF+UHgobt5YzI2Pkw/H39ou9suW687MY6PmCwPmkNV/4TNjn1p2tX5xO3j0d0sq5hiYE24bSlg==} + dev: false + /js-base64@3.7.7: resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} dev: false