Skip to content

Commit

Permalink
feat: add ES256Signer (#240)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshambaugh authored Aug 7, 2022
1 parent 0b94248 commit 08b2761
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 0 deletions.
71 changes: 71 additions & 0 deletions src/__tests__/ES256Signer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { hexToBytes, base58ToBytes, base64ToBytes } from '../util'
import { ES256Signer } from '../signers/ES256Signer'

describe('Secp256r1 Signer', () => {
it('signs data, given a hex private key', async () => {
expect.assertions(1)
const privateKey = '040f1dbf0a2ca86875447a7c010b0fc6d39d76859c458fbe8f2bf775a40ad74a'
const signer = ES256Signer(hexToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'vOTe64WujVUjEiQrAlwaPJtNADx4usSlCfe8OXHS6Np1BqJdqdJX912pVwVlAjmbqR_TMVE5i5TWB_GJVgrHgg'
)
})

it('signs data: privateKey with 0x prefix', async () => {
expect.assertions(1)
const privateKey = '0x040f1dbf0a2ca86875447a7c010b0fc6d39d76859c458fbe8f2bf775a40ad74a'
const signer = ES256Signer(hexToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'vOTe64WujVUjEiQrAlwaPJtNADx4usSlCfe8OXHS6Np1BqJdqdJX912pVwVlAjmbqR_TMVE5i5TWB_GJVgrHgg'
)
})

it('signs data: privateKey base58', async () => {
expect.assertions(1)
const privateKey = 'Gqzym8nfnxR5ZYZ3wZo8rvTwKTqGn5cJsbHnEhUZDPo'
const signer = ES256Signer(base58ToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'vOTe64WujVUjEiQrAlwaPJtNADx4usSlCfe8OXHS6Np1BqJdqdJX912pVwVlAjmbqR_TMVE5i5TWB_GJVgrHgg'
)
})

it('signs data: privateKey base64url', async () => {
expect.assertions(1)
const privateKey = 'BA8dvwosqGh1RHp8AQsPxtOddoWcRY--jyv3daQK10o'
const signer = ES256Signer(base64ToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'vOTe64WujVUjEiQrAlwaPJtNADx4usSlCfe8OXHS6Np1BqJdqdJX912pVwVlAjmbqR_TMVE5i5TWB_GJVgrHgg'
)
})

it('signs data: privateKey base64', async () => {
expect.assertions(1)
const privateKey = 'BA8dvwosqGh1RHp8AQsPxtOddoWcRY++jyv3daQK10o'
const signer = ES256Signer(base64ToBytes(privateKey))
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
await expect(signer(plaintext)).resolves.toEqual(
'vOTe64WujVUjEiQrAlwaPJtNADx4usSlCfe8OXHS6Np1BqJdqdJX912pVwVlAjmbqR_TMVE5i5TWB_GJVgrHgg'
)
})

it('refuses wrong key size (too short)', async () => {
expect.assertions(1)
const privateKey = '040f1dbf0a2ca86875447a7c010b0fc6d39d76859c458fbe8f2bf775a40ad7'
expect(() => {
ES256Signer(hexToBytes(privateKey))
}).toThrowError(/^bad_key: Invalid private key format.*/)
})

it('refuses wrong key size (double)', async () => {
expect.assertions(1)
const privateKey =
'040f1dbf0a2ca86875447a7c010b0fc6d39d76859c458fbe8f2bf775a40ad74a040f1dbf0a2ca86875447a7c010b0fc6d39d76859c458fbe8f2bf775a40ad74a'
expect(() => {
ES256Signer(hexToBytes(privateKey))
}).toThrowError(/^bad_key: Invalid private key format.*/)
})
})
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import SimpleSigner from './signers/SimpleSigner'
import EllipticSigner from './signers/EllipticSigner'
import NaclSigner from './signers/NaclSigner'
import { ES256KSigner } from './signers/ES256KSigner'
import { ES256Signer } from './signers/ES256Signer'
import { EdDSASigner } from './signers/EdDSASigner'
import {
verifyJWT,
Expand Down Expand Up @@ -35,6 +36,7 @@ export {
SimpleSigner,
EllipticSigner,
NaclSigner,
ES256Signer,
ES256KSigner,
EdDSASigner,
verifyJWT,
Expand Down
42 changes: 42 additions & 0 deletions src/signers/ES256Signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { leftpad } from '../util'
import { toJose } from '../util'
import { Signer } from '../JWT'
import { sha256 } from '../Digest'
import elliptic from 'elliptic'

const secp256r1 = new elliptic.ec('p256')

/**
* Creates a configured signer function for signing data using the ES256K (secp256r1 + sha256) algorithm.
*
* The signing function itself takes the data as a `Uint8Array` or `string` and returns a `base64Url`-encoded signature
*
* @example
* ```typescript
* const sign: Signer = ES256Signer(process.env.PRIVATE_KEY)
* const signature: string = await sign(data)
* ```
*
* @param {String} privateKey a private key as `Uint8Array`
* @param {Boolean} recoverable an optional flag to add the recovery param to the generated signatures
* @return {Function} a configured signer function `(data: string | Uint8Array): Promise<string>`
*/
export function ES256Signer(privateKey: Uint8Array, recoverable = false): Signer {
const privateKeyBytes: Uint8Array = privateKey
if (privateKeyBytes.length !== 32) {
throw new Error(`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKeyBytes.length}`)
}
const keyPair: elliptic.ec.KeyPair = secp256r1.keyFromPrivate(privateKeyBytes)

return async (data: string | Uint8Array): Promise<string> => {
const { r, s, recoveryParam }: elliptic.ec.Signature = keyPair.sign(sha256(data))
return toJose(
{
r: leftpad(r.toString('hex')),
s: leftpad(s.toString('hex')),
recoveryParam,
},
recoverable
)
}
}

0 comments on commit 08b2761

Please sign in to comment.