diff --git a/apps/policy-engine/src/engine/core/service/signing.service.ts b/apps/policy-engine/src/engine/core/service/signing.service.ts deleted file mode 100644 index 91e354463..000000000 --- a/apps/policy-engine/src/engine/core/service/signing.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Payload, PrivateKey, SigningAlg, buildSignerEip191, buildSignerEs256k, signJwt } from '@narval/signature' -import { Injectable } from '@nestjs/common' -type SignOptions = { - alg?: SigningAlg -} - -@Injectable() -export class SigningService { - constructor() {} - - async sign(payload: Payload, jwk: PrivateKey, opts: SignOptions = {}): Promise { - const alg: SigningAlg = opts.alg || jwk.alg - if (alg === SigningAlg.ES256K) { - const pk = jwk.d - const jwt = await signJwt(payload, jwk, opts, buildSignerEs256k(pk)) - - return jwt - } else if (alg === SigningAlg.EIP191) { - const pk = jwk.d - - const jwt = await signJwt(payload, jwk, opts, buildSignerEip191(pk)) - - return jwt - } - - throw new Error('Unsupported algorithm') - } -} diff --git a/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts b/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts index 89c1d7fa9..c73d1f8ec 100644 --- a/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts +++ b/apps/policy-engine/src/open-policy-agent/core/open-policy-agent.engine.ts @@ -18,8 +18,6 @@ import { PrivateKey, PublicKey, SigningAlg, - base64UrlToHex, - buildSignerEip191, decode, hash, privateKeyToJwk, @@ -320,6 +318,6 @@ export class OpenPolicyAgentEngine implements Engine { }) } - return signJwt(payload, engineJwk, { alg: SigningAlg.EIP191 }, buildSignerEip191(base64UrlToHex(engineJwk.d))) + return signJwt(payload, engineJwk, { alg: SigningAlg.EIP191 }) } } diff --git a/apps/policy-engine/src/shared/testing/evaluation.testing.ts b/apps/policy-engine/src/shared/testing/evaluation.testing.ts index 861268e2f..c7774cb43 100644 --- a/apps/policy-engine/src/shared/testing/evaluation.testing.ts +++ b/apps/policy-engine/src/shared/testing/evaluation.testing.ts @@ -28,9 +28,9 @@ export const generateInboundEvaluationRequest = async (): Promise { }) it('should build & sign a EIP191 JWT', async () => { - const jwk = ellipticPrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) + const jwk = secp256k1PrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) const signer = buildSignerEip191(UNSAFE_PRIVATE_KEY) const jwt = await signJwt(payload, jwk, { alg: SigningAlg.EIP191 }, signer) @@ -157,7 +159,7 @@ describe('sign', () => { const viemPubKey = privateKeyToAccount(`0x${UNSAFE_PRIVATE_KEY}`).publicKey expect(toHex(publicKey)).toBe(viemPubKey) // Confirm that our key is in fact the same as what viem would give. - const jwk = ellipticPrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) + const jwk = secp256k1PrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) const k = await createPublicKey({ format: 'jwk', @@ -168,7 +170,7 @@ describe('sign', () => { }) it('should convert to and from jwk', async () => { - const jwk = ellipticPrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) + const jwk = secp256k1PrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) const pk = ellipticPrivateKeyToHex(jwk) expect(pk).toBe(`0x${UNSAFE_PRIVATE_KEY}`) }) @@ -176,7 +178,60 @@ describe('sign', () => { it('should convert to and from public jwk', async () => { const publicKey = secp256k1.getPublicKey(UNSAFE_PRIVATE_KEY, false) const jwk = secp256k1PublicKeyToJwk(toHex(publicKey)) - const pk = ellicpticPublicKeyToHex(jwk) + const pk = ellipticPublicKeyToHex(jwk) expect(pk).toBe(toHex(publicKey)) }) + + it('should sign using a custom signer and a key without material', async () => { + const privateKey = secp256k1PrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) + const mapOfKeys = new Map().set(privateKey.kid, privateKey) + + function createSignerWithKid(kid: string): (message: string) => Promise { + return async (message: string) => { + const privateKeyMaterial = mapOfKeys.get(kid) + if (!privateKeyMaterial) { + throw new Error(`Key material for kid ${kid} not found.`) + } + const hash = sha256Hash(message) + const key = privateKeyToHex(privateKeyMaterial) + + const signature = signSecp256k1(hash, key) + const hexSignature = signatureToHex(signature) + return hexToBase64Url(hexSignature) + } + } + // Custom function that mimic a custom signer that would fetch the key material from the kid + + const signer = createSignerWithKid(privateKey.kid) + // It would need to have kid context before being passed to signing + // TODO: maybe signer should take both the message and the key + + const jwt = await signJwt(payload, privateKey, { alg: SigningAlg.ES256K }, signer) + const verified = await verifyJwt(jwt, privateKey) + expect(verified.payload).toEqual(payload) + }) + + it('should sign with default signer and specified algorithm', async () => { + const privateKey = secp256k1PrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) + const jwt = await signJwt(payload, privateKey, { alg: SigningAlg.EIP191 }) + const verified = await verifyJwt(jwt, privateKey) + expect(verified.payload).toEqual(payload) + }) + + it('should sign with default signer and no options provided', async () => { + const privateKey = secp256k1PrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) + const jwt = await signJwt(payload, privateKey) + const verified = await verifyJwt(jwt, privateKey) + expect(verified.payload).toEqual(payload) + }) + + it('throws if specified algorithm is not supported by the key', async () => { + const privateKey = secp256k1PrivateKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) + await expect(signJwt(payload, privateKey, { alg: SigningAlg.ES256 })).rejects.toThrow() + }) + + it('throws if no signer is provided and key material is not present', async () => { + const publicKey = secp256k1PublicKeyToJwk(`0x${UNSAFE_PRIVATE_KEY}`) + await expect(signJwt(payload, publicKey)).rejects.toThrow() + }) }) diff --git a/packages/signature/src/lib/__test__/unit/verify.spec.ts b/packages/signature/src/lib/__test__/unit/verify.spec.ts index c18fcf388..f09e5c18e 100644 --- a/packages/signature/src/lib/__test__/unit/verify.spec.ts +++ b/packages/signature/src/lib/__test__/unit/verify.spec.ts @@ -3,7 +3,7 @@ import { hash } from '../../hash-request' import { secp256k1PublicKeySchema } from '../../schemas' import { signSecp256k1 } from '../../sign' import { Alg, Payload, Secp256k1PublicKey } from '../../types' -import { ellipticPrivateKeyToJwk, privateKeyToJwk } from '../../utils' +import { privateKeyToJwk, secp256k1PrivateKeyToJwk } from '../../utils' import { validate } from '../../validate' import { verifyJwt, verifySepc256k1 } from '../../verify' @@ -11,7 +11,7 @@ describe('verify', () => { const ENGINE_PRIVATE_KEY = '7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5' it('should verify a EIP191-signed JWT', async () => { - const jwk = ellipticPrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`) + const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`) const header = { kid: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1', diff --git a/packages/signature/src/lib/schemas.ts b/packages/signature/src/lib/schemas.ts index 1e6ac7744..8fdf1eb8b 100644 --- a/packages/signature/src/lib/schemas.ts +++ b/packages/signature/src/lib/schemas.ts @@ -84,6 +84,8 @@ export const secp256k1KeySchema = z.union([secp256k1PublicKeySchema, secp256k1Pr export const p256KeySchema = z.union([p256PublicKeySchema, p256PrivateKeySchema]) +export const ellipticKeySchema = z.union([secp256k1KeySchema, p256KeySchema]) + const dynamicKeySchema = z.object({}).catchall(z.unknown()) export const jwkSchema = dynamicKeySchema.extend({ diff --git a/packages/signature/src/lib/sign.ts b/packages/signature/src/lib/sign.ts index f8e277391..0c0598789 100644 --- a/packages/signature/src/lib/sign.ts +++ b/packages/signature/src/lib/sign.ts @@ -69,22 +69,40 @@ export async function signJwt( */ export async function signJwt(payload: Payload, jwk: Jwk, opts: { alg?: SigningAlg }): Promise +/** + * Signs using default signers per algorithm. Key private material is required. + * opts are not provided + * @param payload + * @param jwk + * @returns + */ +export async function signJwt(payload: Payload, jwk: Jwk): Promise + export async function signJwt( payload: Payload, jwk: Jwk, - opts: { alg?: SigningAlg } = {}, + optsOrSigner?: { alg?: SigningAlg } | ((payload: string) => Promise), signer?: (payload: string) => Promise ): Promise { + let opts: { alg?: SigningAlg } = {} + let actualSigner: ((payload: string) => Promise) | undefined = undefined + + if (typeof optsOrSigner === 'function') { + actualSigner = optsOrSigner + } else { + opts = optsOrSigner || {} + actualSigner = signer + } + const header = buildHeader(jwk, opts.alg) const encodedHeader = base64url.encode(JSON.stringify(header)) const encodedPayload = base64url.encode(JSON.stringify(payload)) const messageToSign = `${encodedHeader}.${encodedPayload}` - // Signature logic based on the presence of a custom signer + // Determine the signing logic based on the presence of a custom signer let signature: string - if (signer) { - // Custom signer provided - signature = await signer(messageToSign) + if (actualSigner) { + signature = await actualSigner(messageToSign) } else { // Default signer logic // Validate JWK as a private key for default signing @@ -105,6 +123,7 @@ export async function signJwt( return fallbackSigner(privateKey, payload, header) } } + return `${messageToSign}.${signature}` } diff --git a/packages/signature/src/lib/types.ts b/packages/signature/src/lib/types.ts index 9a657724a..1bfe18519 100644 --- a/packages/signature/src/lib/types.ts +++ b/packages/signature/src/lib/types.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { + ellipticKeySchema, jwkBaseSchema, jwkEoaSchema, jwkSchema, @@ -63,6 +64,7 @@ export type RsaKey = RsaPrivateKey | RsaPublicKey export type EoaPublicKey = z.infer export type Secp256k1PublicKey = z.infer export type Secp256k1Key = z.infer +export type EllipticKey = z.infer export type PublicKey = z.infer export type PrivateKey = z.infer export type PartialJwk = z.infer diff --git a/packages/signature/src/lib/utils.ts b/packages/signature/src/lib/utils.ts index 925e7db8e..e9c555f71 100644 --- a/packages/signature/src/lib/utils.ts +++ b/packages/signature/src/lib/utils.ts @@ -5,10 +5,11 @@ import { exportJWK, generateKeyPair } from 'jose' import { toHex } from 'viem' import { publicKeyToAddress } from 'viem/utils' import { JwtError } from './error' -import { privateKeySchema, rsaPrivateKeySchema, rsaPublicKeySchema, secp256k1KeySchema } from './schemas' +import { ellipticKeySchema, privateKeySchema, rsaPrivateKeySchema, rsaPublicKeySchema } from './schemas' import { Alg, Curves, + EllipticKey, Hex, Jwk, KeyTypes, @@ -17,7 +18,6 @@ import { PrivateKey, RsaPrivateKey, RsaPublicKey, - Secp256k1Key, Secp256k1PrivateKey, Secp256k1PublicKey, Use @@ -90,7 +90,7 @@ export const p256PublicKeyToJwk = (publicKey: Hex, keyId?: string): P256PublicKe } // ES256k -export const ellipticPrivateKeyToJwk = (privateKey: Hex, keyId?: string): Secp256k1PrivateKey => { +export const secp256k1PrivateKeyToJwk = (privateKey: Hex, keyId?: string): Secp256k1PrivateKey | P256PrivateKey => { const publicKey = toHex(secp256k1.getPublicKey(privateKey.slice(2), false)) const publicJwk = secp256k1PublicKeyToJwk(publicKey, keyId) return { @@ -108,9 +108,9 @@ export const p256PrivateKeyToJwk = (privateKey: Hex, keyId?: string): P256Privat } } -export const ellicpticPublicKeyToHex = (jwk: Jwk): Hex => { - const key = validate({ - schema: secp256k1KeySchema, +export const ellipticPublicKeyToHex = (jwk: Jwk): Hex => { + const key = validate({ + schema: ellipticKeySchema, jwk: jwk, errorMessage: 'Invalid Public Key' }) @@ -134,6 +134,20 @@ export const ellicpticPublicKeyToHex = (jwk: Jwk): Hex => { return `0x04${x.slice(2)}${y.slice(2)}` } +export const publicKeyToJwk = (jwk: Hex, alg: Alg): Jwk => { + switch (alg) { + case Alg.ES256K: + return secp256k1PublicKeyToJwk(jwk) + case Alg.ES256: + return p256PublicKeyToJwk(jwk) + case Alg.RS256: + throw new JwtError({ + message: 'Conversion from Hex to JWK not supported for RSA keys', + context: { jwk } + }) + } +} + export const ellipticPrivateKeyToHex = (jwk: P256PrivateKey | Secp256k1PrivateKey): Hex => { return base64UrlToHex(jwk.d) } @@ -199,7 +213,7 @@ const rsaPrivateKeyToHex = (jwk: Jwk): Hex => { export const publicKeyToHex = (jwk: Jwk): Hex => { if (jwk.kty === KeyTypes.EC) { - return ellicpticPublicKeyToHex(jwk) + return ellipticPublicKeyToHex(jwk) } return rsaPubKeyToHex(jwk) } @@ -219,7 +233,7 @@ export const privateKeyToHex = (jwk: Jwk): Hex => { export const privateKeyToJwk = (jwk: Hex, alg: Alg): PrivateKey => { switch (alg) { case Alg.ES256K: - return ellipticPrivateKeyToJwk(jwk) + return secp256k1PrivateKeyToJwk(jwk) case Alg.ES256: return p256PrivateKeyToJwk(jwk) case Alg.RS256: @@ -282,7 +296,7 @@ export const generateJwk = async ( switch (alg) { case Alg.ES256K: { const privateKeyK1 = toHex(secp256k1.utils.randomPrivateKey()) - return ellipticPrivateKeyToJwk(privateKeyK1, opts?.keyId) as T + return secp256k1PrivateKeyToJwk(privateKeyK1, opts?.keyId) as T } case Alg.ES256: { const privateKeyP256 = toHex(p256.utils.randomPrivateKey()) diff --git a/packages/signature/src/lib/verify.ts b/packages/signature/src/lib/verify.ts index 83b6b5137..ea7739d4d 100644 --- a/packages/signature/src/lib/verify.ts +++ b/packages/signature/src/lib/verify.ts @@ -6,7 +6,7 @@ import { JwtError } from './error' import { eip191Hash } from './sign' import { isSepc256k1PublicKeyJwk } from './typeguards' import { Alg, Hex, Jwk, Jwt, Payload, PublicKey, Secp256k1PublicKey, SigningAlg } from './types' -import { base64UrlToHex, ellicpticPublicKeyToHex, publicKeyToHex } from './utils' +import { base64UrlToHex, ellipticPublicKeyToHex, publicKeyToHex } from './utils' const checkTokenExpiration = (payload: Payload): boolean => { const now = Math.floor(Date.now() / 1000) @@ -45,7 +45,7 @@ const verifyEip191WithPublicKey = async (sig: Hex, hash: Uint8Array, jwk: Public } export const verifySepc256k1 = async (sig: Hex, hash: Uint8Array, jwk: Secp256k1PublicKey): Promise => { - const pubKey = ellicpticPublicKeyToHex(jwk) + const pubKey = ellipticPublicKeyToHex(jwk) const isValid = secp256k1.verify(sig.slice(2, 130), hash, pubKey.slice(2)) === true return isValid } diff --git a/packages/transaction-request-intent/src/lib/intent.types.ts b/packages/transaction-request-intent/src/lib/intent.types.ts index c1dce01c7..796ea4854 100644 --- a/packages/transaction-request-intent/src/lib/intent.types.ts +++ b/packages/transaction-request-intent/src/lib/intent.types.ts @@ -1,7 +1,7 @@ -import { AccountId, AssetId, Hex } from '@narval/policy-engine-shared' +import { AccountId, AssetId, Eip712Domain, Hex } from '@narval/policy-engine-shared' import { Alg } from '@narval/signature' import { Address } from 'viem' -import { Eip712Domain, Intents } from './domain' +import { Intents } from './domain' export type TransferNative = { type: Intents.TRANSFER_NATIVE