Skip to content

Commit

Permalink
merge refactor credential
Browse files Browse the repository at this point in the history
  • Loading branch information
Ptroger committed Mar 21, 2024
2 parents 98bb5ff + 7aa458b commit c06752c
Show file tree
Hide file tree
Showing 18 changed files with 377 additions and 163 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Feed, HistoricalTransfer, JwtString } from '@narval/policy-engine-shared'
import { Payload, SigningAlg, hash, hexToBase64Url, privateKeyToJwk, signJwt } from '@narval/signature'
import { Payload, SigningAlg, hash, hexToBase64Url, secp256k1PrivateKeyToJwk, signJwt } from '@narval/signature'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { mapValues, omit } from 'lodash/fp'
Expand Down Expand Up @@ -37,7 +37,7 @@ export class HistoricalTransferFeedService implements DataFeed<HistoricalTransfe
}

const now = Math.floor(Date.now() / 1000)
const jwk = privateKeyToJwk(this.getPrivateKey())
const jwk = secp256k1PrivateKeyToJwk(this.getPrivateKey())
const payload: Payload = {
data: hash(data),
sub: account.address,
Expand Down
4 changes: 2 additions & 2 deletions apps/armory/src/data-feed/core/service/price-feed.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Action, AssetId, Feed, JwtString } from '@narval/policy-engine-shared'
import { Payload, SigningAlg, hash, hexToBase64Url, privateKeyToJwk, signJwt } from '@narval/signature'
import { Payload, SigningAlg, hash, hexToBase64Url, secp256k1PrivateKeyToJwk, signJwt } from '@narval/signature'
import { InputType, Intents, safeDecode } from '@narval/transaction-request-intent'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
Expand Down Expand Up @@ -36,7 +36,7 @@ export class PriceFeedService implements DataFeed<Prices> {
}

const now = Math.floor(Date.now() / 1000)
const jwk = privateKeyToJwk(this.getPrivateKey())
const jwk = secp256k1PrivateKeyToJwk(this.getPrivateKey())
const payload: Payload = {
data: hash(data),
sub: account.address,
Expand Down
17 changes: 6 additions & 11 deletions apps/policy-engine/src/engine/core/service/signing.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { JsonWebKey, toHex } from '@narval/policy-engine-shared'
import { toHex } from '@narval/policy-engine-shared'
import {
Alg,
Payload,
PrivateKey,
PublicKey,
SigningAlg,
buildSignerEip191,
buildSignerEs256k,
Expand All @@ -17,8 +19,8 @@ type KeyGenerationOptions = {
}

type KeyGenerationResponse = {
publicKey: JsonWebKey
privateKey?: JsonWebKey
publicKey: PublicKey
privateKey?: PrivateKey
}

type SignOptions = {
Expand Down Expand Up @@ -49,21 +51,14 @@ export class SigningService {
throw new Error('Unsupported algorithm')
}

async sign(payload: Payload, jwk: JsonWebKey, opts: SignOptions = {}): Promise<string> {
async sign(payload: Payload, jwk: PrivateKey, opts: SignOptions = {}): Promise<string> {
const alg: SigningAlg = opts.alg || jwk.alg
if (alg === SigningAlg.ES256K) {
if (!jwk.d) {
throw new Error('Missing private key')
}
const pk = jwk.d

const jwt = await signJwt(payload, jwk, opts, buildSignerEs256k(pk))

return jwt
} else if (alg === SigningAlg.EIP191) {
if (!jwk.d) {
throw new Error('Missing private key')
}
const pk = jwk.d

const jwt = await signJwt(payload, jwk, opts, buildSignerEip191(pk))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ import {
Entities,
EvaluationRequest,
EvaluationResponse,
JsonWebKey,
JwtString,
Policy
} from '@narval/policy-engine-shared'
import {
Hex,
Jwk,
Payload,
SigningAlg,
base64UrlToHex,
buildSignerEip191,
decode,
hash,
privateKeyToJwk,
publicKeyToJwk,
secp256k1PrivateKeyToJwk,
signJwt,
verifyJwt
} from '@narval/signature'
Expand Down Expand Up @@ -187,9 +186,9 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
})
}

const jwk = publicKeyToJwk(credential.pubKey as Hex)
const { key } = credential

const validJwt = await verifyJwt(signature, jwk)
const validJwt = await verifyJwt(signature, key)

if (validJwt.payload.requestHash !== message) {
throw new OpenPolicyAgentException({
Expand Down Expand Up @@ -304,8 +303,8 @@ export class OpenPolicyAgentEngine implements Engine<OpenPolicyAgentEngine> {
}

private async sign(params: { principalCredential: CredentialEntity; message: string }): Promise<JwtString> {
const engineJwk: JsonWebKey = privateKeyToJwk(this.privateKey)
const principalJwk = publicKeyToJwk(params.principalCredential.pubKey as Hex)
const engineJwk: Jwk = secp256k1PrivateKeyToJwk(this.privateKey)
const principalJwk: Jwk = params.principalCredential.key

const payload: Payload = {
requestHash: params.message,
Expand Down
77 changes: 52 additions & 25 deletions packages/policy-engine-shared/src/lib/dev.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alg, addressToKid } from '@narval/signature'
import { Alg, Curves, KeyTypes, Use } from '@narval/signature'
import { PrivateKeyAccount } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { Action } from './type/action.type'
Expand Down Expand Up @@ -81,39 +81,66 @@ export const USER: Record<Personas, UserEntity> = {

export const CREDENTIAL: Record<Personas, CredentialEntity> = {
Root: {
id: addressToKid(ACCOUNT.Root.address),
pubKey: ACCOUNT.Root.publicKey,
address: ACCOUNT.Root.address,
alg: Alg.ES256K,
userId: USER.Root.id
id: '0x20FB9603DC2C011aBFdFbf270bD627e94065cBb9',
userId: USER.Root.id,
key: {
kty: KeyTypes.EC,
use: Use.ENC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0x20FB9603DC2C011aBFdFbf270bD627e94065cBb9',
x: 'crqZ2XkCBgl1XwxjlQ02PKm_JJ4wJAkANJ6DidZRzTw',
y: 'GyAbgM5_HOaPmAHNatWanWmhLgaznyNHUIw5YUe_yyw'
}
},
Alice: {
id: addressToKid(ACCOUNT.Alice.address),
pubKey: ACCOUNT.Alice.publicKey,
address: ACCOUNT.Alice.address,
alg: Alg.ES256K,
userId: USER.Alice.id
id: '0xcdE93dc1C6D8AF279c33069233aEE5542F308594',
userId: USER.Alice.id,
key: {
kty: KeyTypes.EC,
use: Use.SIG,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0xcdE93dc1C6D8AF279c33069233aEE5542F308594',
x: 'vjNVzbnLxdazY0M-2BDnX54JexB8Pa9n_fucDJli6Bo',
y: 'jOAwUCXcLz7nhvW2mSwPBCZwv856ybAGK7LS6hvfdFQ'
}
},
Bob: {
id: addressToKid(ACCOUNT.Bob.address),
pubKey: ACCOUNT.Bob.publicKey,
address: ACCOUNT.Bob.address,
alg: Alg.ES256K,
id: '0x9A5Bd18C902887DCc2D881a352010C15eea229d',
key: {
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0xc7916Ee805440bB386a88d09AED8688eFb99CB0F',
x: 'MjsuvdMuxs1AoQ12BuARzzTyilJNh2jQmErMZwR2M-E',
y: 'axLms3pGEX0Xujho5welzcn9mx_oV0Bs3uVeG9-eCqU'
},
userId: USER.Bob.id
},
Carol: {
id: addressToKid(ACCOUNT.Carol.address),
pubKey: ACCOUNT.Carol.publicKey,
address: ACCOUNT.Carol.address,
alg: Alg.ES256K,
userId: USER.Carol.id
id: '0xe99c6FBb2eE939682AB8A216a893cBD21CC2f982',
userId: USER.Carol.id,
key: {
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0x9AA5Bd18C902887DCc2D881a352010C15eea229d',
x: '4n3yf5qUBU0sDH9yGjdfiVRFEnQndbd5yGEupSdG6R4',
y: 'FESQhctMSQOF2E79YbCE8q1JIQWltMbvoCVwSsO19ck'
}
},
Dave: {
id: addressToKid(ACCOUNT.Dave.address),
pubKey: ACCOUNT.Dave.publicKey,
address: ACCOUNT.Dave.address,
alg: Alg.ES256K,
userId: USER.Dave.id
id: '0xddd26a02e7c54e8dc373b9d2dcb309ecdeca815d',
userId: USER.Dave.id,
key: {
kty: KeyTypes.EC,
crv: Curves.SECP256K1,
alg: Alg.ES256K,
kid: '0xe99c6FBb2eE939682AB8A216a893cBD21CC2f982',
x: 'sdb8VZcfcI6t5i7BD3BTPoZPyYCxaVpw7H1BIUyPZ5M',
y: 'cIcYdzuWF7KqFKJrdQSmdjPpQzrk9_uzNycqtvtH1QI'
}
}
}

Expand Down
9 changes: 4 additions & 5 deletions packages/policy-engine-shared/src/lib/schema/entity.schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alg } from '@narval/signature'
import { publicKeySchema } from '@narval/signature'
import { z } from 'zod'
import { addressSchema } from './address.schema'

Expand All @@ -23,10 +23,9 @@ export const accountClassificationSchema = z.nativeEnum({

export const credentialEntitySchema = z.object({
id: z.string(),
pubKey: z.string(),
address: z.string().optional(),
alg: z.nativeEnum(Alg),
userId: z.string()
userId: z.string(),
key: publicKeySchema
// TODO @ptroger: Should we be allowing a private key to be passed in entity data ?
})

export const organizationEntitySchema = z.object({
Expand Down
1 change: 1 addition & 0 deletions packages/signature/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './lib/decode'
export * from './lib/hash-request'
export * from './lib/schemas'
export * from './lib/sign'
export * from './lib/types'
export * from './lib/utils'
Expand Down
24 changes: 12 additions & 12 deletions packages/signature/src/lib/__test__/unit/sign.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { createPublicKey } from 'node:crypto'
import { toHex, verifyMessage } from 'viem'
import { privateKeyToAccount, signMessage } from 'viem/accounts'
import { buildSignerEip191, buildSignerEs256k, signJwt } from '../../sign'
import { Alg, JWK, Payload, SigningAlg } from '../../types'
import { Alg, Payload, PrivateKey, SigningAlg } from '../../types'
import {
base64UrlToBytes,
base64UrlToHex,
jwkToPrivateKey,
jwkToPublicKey,
privateKeyToJwk,
publicKeyToJwk
secp256k1PrivateKeyToHex,
secp256k1PrivateKeyToJwk,
secp256k1PublicKeyToHex,
secp256k1PublicKeyToJwk
} from '../../utils'
import { verifyJwt } from '../../verify'
import { HEADER_PART, PAYLOAD_PART, PRIVATE_KEY_PEM } from './mock'
Expand All @@ -38,14 +38,14 @@ describe('sign', () => {
it('should sign build & sign es256 JWT correctly with a PEM', async () => {
const key = await importPKCS8(PRIVATE_KEY_PEM, Alg.ES256)
const jwk = await exportJWK(key)
const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256 } as JWK)
const jwt = await signJwt(payload, { ...jwk, alg: Alg.ES256 } as PrivateKey)

const verified = await jwtVerify(jwt, key)
expect(verified.payload).toEqual(payload)
})

it('should build & sign a EIP191 JWT', async () => {
const jwk = privateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const signer = buildSignerEip191(ENGINE_PRIVATE_KEY)

const jwt = await signJwt(payload, jwk, { alg: SigningAlg.EIP191 }, signer)
Expand Down Expand Up @@ -146,7 +146,7 @@ describe('sign', () => {
const viemPubKey = privateKeyToAccount(`0x${ENGINE_PRIVATE_KEY}`).publicKey
expect(toHex(publicKey)).toBe(viemPubKey) // Confirm that our key is in fact the same as what viem would give.

const jwk = privateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)

const k = await createPublicKey({
format: 'jwk',
Expand All @@ -157,15 +157,15 @@ describe('sign', () => {
})

it('should convert to and from jwk', async () => {
const jwk = privateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const pk = jwkToPrivateKey(jwk)
const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const pk = secp256k1PrivateKeyToHex(jwk)
expect(pk).toBe(`0x${ENGINE_PRIVATE_KEY}`)
})

it('should convert to and from public jwk', async () => {
const publicKey = secp256k1.getPublicKey(ENGINE_PRIVATE_KEY, false)
const jwk = publicKeyToJwk(toHex(publicKey))
const pk = jwkToPublicKey(jwk)
const jwk = secp256k1PublicKeyToJwk(toHex(publicKey))
const pk = secp256k1PublicKeyToHex(jwk)
expect(pk).toBe(toHex(publicKey))
})
})
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
@@ -1,13 +1,13 @@
import { hash } from '../../hash-request'
import { Payload } from '../../types'
import { privateKeyToJwk } from '../../utils'
import { secp256k1PrivateKeyToJwk } from '../../utils'
import { verifyJwt } from '../../verify'

describe('verify', () => {
const ENGINE_PRIVATE_KEY = '7cfef3303797cbc7515d9ce22ffe849c701b0f2812f999b0847229c47951fca5'

it('should verify a EIP191-signed JWT', async () => {
const jwk = privateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)
const jwk = secp256k1PrivateKeyToJwk(`0x${ENGINE_PRIVATE_KEY}`)

const header = {
kid: '0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
Expand Down
22 changes: 22 additions & 0 deletions packages/signature/src/lib/address.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod'
import { isAddress } from './evm.util'

/**
* Schema backward compatible with viem's Address type.
*
* @see https://viem.sh/docs/glossary/types#address
*/
export const addressSchema = z.custom<`0x${string}`>(
(value) => {
const parse = z.string().safeParse(value)

if (parse.success) {
return isAddress(parse.data)
}

return false
},
{
message: 'value is an invalid Ethereum address'
}
)
47 changes: 47 additions & 0 deletions packages/signature/src/lib/evm.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// eslint-disable-next-line no-restricted-imports
import { InvalidAddressError, getAddress as viemGetAddress, isAddress as viemIsAddress } from 'viem'

type Address = `0x${string}`

/**
* Checks if a string is a valid Ethereum address without regard of its format.
*
* @param address - The string to be checked.
* @returns Returns true if the string is a valid Ethereum address, otherwise
* returns false.
*/
export const isAddress = (address: string): boolean => {
if (!/^(0x)?[0-9a-fA-F]{40}$/.test(address)) {
return false
} else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) {
return true
} else {
return viemIsAddress(address)
}
}

/**
* Retrieves the Ethereum address from a given string representation without
* regard of its format.
*
* @param address - The string representation of the Ethereum address.
* @param options - Optional parameters for address retrieval.
* @param options.checksum - Specifies whether the retrieved address should be
* checksummed.
* @param options.chainId - The chain ID to be used for address retrieval.
* @returns The Ethereum address.
* @throws {InvalidAddressError} if the provided address is invalid.
*/
export const getAddress = (address: string, options?: { checksum?: boolean; chainId?: number }): Address => {
if (isAddress(address)) {
const validAddress = address as Address

if (options?.checksum || options?.chainId) {
return viemGetAddress(validAddress, options.chainId)
}

return validAddress
}

throw new InvalidAddressError({ address })
}
Loading

0 comments on commit c06752c

Please sign in to comment.