Skip to content

Commit

Permalink
Test coverage on different signing calls
Browse files Browse the repository at this point in the history
  • Loading branch information
Ptroger committed Mar 29, 2024
1 parent b826550 commit 4953393
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 64 deletions.
28 changes: 0 additions & 28 deletions apps/policy-engine/src/engine/core/service/signing.service.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import {
PrivateKey,
PublicKey,
SigningAlg,
base64UrlToHex,
buildSignerEip191,
decode,
hash,
privateKeyToJwk,
Expand Down Expand Up @@ -320,6 +318,6 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
})
}

return signJwt(payload, engineJwk, { alg: SigningAlg.EIP191 }, buildSignerEip191(base64UrlToHex(engineJwk.d)))
return signJwt(payload, engineJwk, { alg: SigningAlg.EIP191 })
}
}
6 changes: 3 additions & 3 deletions apps/policy-engine/src/shared/testing/evaluation.testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export const generateInboundEvaluationRequest = async (): Promise<EvaluationRequ
requestHash: message
}

const aliceSignature = await signJwt(payload, privateKeyToJwk(UNSAFE_PRIVATE_KEY.Alice, Alg.ES256K), {})
const bobSignature = await signJwt(payload, privateKeyToJwk(UNSAFE_PRIVATE_KEY.Bob, Alg.ES256K), {})
const carolSignature = await signJwt(payload, privateKeyToJwk(UNSAFE_PRIVATE_KEY.Carol, Alg.ES256K), {})
const aliceSignature = await signJwt(payload, privateKeyToJwk(UNSAFE_PRIVATE_KEY.Alice, Alg.ES256K))
const bobSignature = await signJwt(payload, privateKeyToJwk(UNSAFE_PRIVATE_KEY.Bob, Alg.ES256K))
const carolSignature = await signJwt(payload, privateKeyToJwk(UNSAFE_PRIVATE_KEY.Carol, Alg.ES256K))

return {
authentication: aliceSignature,
Expand Down
2 changes: 1 addition & 1 deletion packages/signature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export * from './lib/hash-request'
export * from './lib/schemas'
export * from './lib/sign'
export * from './lib/types'
export * from './lib/utils'
export { generateJwk, privateKeyToHex, privateKeyToJwk, publicKeyToHex, publicKeyToJwk } from './lib/utils'
export * from './lib/verify'
73 changes: 64 additions & 9 deletions packages/signature/src/lib/__test__/unit/sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { secp256k1 } from '@noble/curves/secp256k1'
import { sha256 as sha256Hash } from '@noble/hashes/sha256'
import { exportJWK, importPKCS8 } from 'jose'
import { createPublicKey } from 'node:crypto'
import { toHex, verifyMessage } from 'viem'
import { signatureToHex, toHex, verifyMessage } from 'viem'
import { privateKeyToAccount, signMessage } from 'viem/accounts'
import { buildSignerEip191, buildSignerEs256k, signJwt } from '../../sign'
import { Alg, Curves, Jwk, KeyTypes, Payload, SigningAlg } from '../../types'
import { buildSignerEip191, buildSignerEs256k, signJwt, signSecp256k1 } from '../../sign'
import { Alg, Curves, Jwk, KeyTypes, Payload, PrivateKey, SigningAlg } from '../../types'
import {
base64UrlToBytes,
base64UrlToHex,
ellicpticPublicKeyToHex,
ellipticPrivateKeyToHex,
ellipticPrivateKeyToJwk,
ellipticPublicKeyToHex,
hexToBase64Url,
privateKeyToHex,
secp256k1PrivateKeyToJwk,
secp256k1PublicKeyToJwk
} from '../../utils'
import { verifyJwt } from '../../verify'
Expand Down Expand Up @@ -56,7 +58,7 @@ describe('sign', () => {
})

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)
Expand Down Expand Up @@ -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',
Expand All @@ -168,15 +170,68 @@ 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}`)
})

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<string, PrivateKey>().set(privateKey.kid, privateKey)

function createSignerWithKid(kid: string): (message: string) => Promise<string> {
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()
})
})
4 changes: 2 additions & 2 deletions packages/signature/src/lib/__test__/unit/verify.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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'

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',
Expand Down
2 changes: 2 additions & 0 deletions packages/signature/src/lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
29 changes: 24 additions & 5 deletions packages/signature/src/lib/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,40 @@ export async function signJwt(
*/
export async function signJwt(payload: Payload, jwk: Jwk, opts: { alg?: SigningAlg }): Promise<string>

/**
* 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<string>

export async function signJwt(
payload: Payload,
jwk: Jwk,
opts: { alg?: SigningAlg } = {},
optsOrSigner?: { alg?: SigningAlg } | ((payload: string) => Promise<string>),
signer?: (payload: string) => Promise<string>
): Promise<string> {
let opts: { alg?: SigningAlg } = {}
let actualSigner: ((payload: string) => Promise<string>) | 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
Expand All @@ -105,6 +123,7 @@ export async function signJwt(
return fallbackSigner(privateKey, payload, header)
}
}

return `${messageToSign}.${signature}`
}

Expand Down
2 changes: 2 additions & 0 deletions packages/signature/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import {
ellipticKeySchema,
jwkBaseSchema,
jwkEoaSchema,
jwkSchema,
Expand Down Expand Up @@ -63,6 +64,7 @@ export type RsaKey = RsaPrivateKey | RsaPublicKey
export type EoaPublicKey = z.infer<typeof jwkEoaSchema>
export type Secp256k1PublicKey = z.infer<typeof secp256k1PublicKeySchema>
export type Secp256k1Key = z.infer<typeof secp256k1KeySchema>
export type EllipticKey = z.infer<typeof ellipticKeySchema>
export type PublicKey = z.infer<typeof publicKeySchema>
export type PrivateKey = z.infer<typeof privateKeySchema>
export type PartialJwk = z.infer<typeof jwkBaseSchema>
Expand Down
32 changes: 23 additions & 9 deletions packages/signature/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,7 +18,6 @@ import {
PrivateKey,
RsaPrivateKey,
RsaPublicKey,
Secp256k1Key,
Secp256k1PrivateKey,
Secp256k1PublicKey,
Use
Expand Down Expand Up @@ -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 {
Expand All @@ -108,9 +108,9 @@ export const p256PrivateKeyToJwk = (privateKey: Hex, keyId?: string): P256Privat
}
}

export const ellicpticPublicKeyToHex = (jwk: Jwk): Hex => {
const key = validate<Secp256k1Key>({
schema: secp256k1KeySchema,
export const ellipticPublicKeyToHex = (jwk: Jwk): Hex => {
const key = validate<EllipticKey>({
schema: ellipticKeySchema,
jwk: jwk,
errorMessage: 'Invalid Public Key'
})
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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:
Expand Down Expand Up @@ -282,7 +296,7 @@ export const generateJwk = async <T = Jwk>(
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())
Expand Down
4 changes: 2 additions & 2 deletions packages/signature/src/lib/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -45,7 +45,7 @@ const verifyEip191WithPublicKey = async (sig: Hex, hash: Uint8Array, jwk: Public
}

export const verifySepc256k1 = async (sig: Hex, hash: Uint8Array, jwk: Secp256k1PublicKey): Promise<boolean> => {
const pubKey = ellicpticPublicKeyToHex(jwk)
const pubKey = ellipticPublicKeyToHex(jwk)
const isValid = secp256k1.verify(sig.slice(2, 130), hash, pubKey.slice(2)) === true
return isValid
}
Expand Down
4 changes: 2 additions & 2 deletions packages/transaction-request-intent/src/lib/intent.types.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 4953393

Please sign in to comment.