Skip to content

Commit

Permalink
feat: Add support for ES* password protected private keys (#119)
Browse files Browse the repository at this point in the history
* Added support for ES* password protected private keys

* Update handling of the password protected keys

* chore: removed unnecessary code and fixed code coverage

* chore: fixed linter issues

* chore: updated Readme

* Update README.md

Co-authored-by: Simone Busoli <simone.busoli@nearform.com>

Co-authored-by: Simone Busoli <simone.busoli@nearform.com>
  • Loading branch information
radomird and simoneb committed Nov 2, 2021
1 parent c5ee92f commit 1ab4ce3
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 25 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The payload must be an object.

If the `key` option is a function, the signer will also accept a Node style callback and will return a promise, supporting therefore both callback and async/await styles.

If the `key` is a passphrase protected private key, then it must be an object with the following structure:
If the `key` is a passphrase protected private key, the `algorithm` option must be provided and must be either a `RS*` or `ES*` encoded key and the `key` option must be an object with the following structure:
```js
{
key: '<YOUR_RSA_ENCRYPTED_PRIVATE_KEY>',
Expand Down
6 changes: 4 additions & 2 deletions benchmarks/keys/generate-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { resolve } = require('path')
const passProtectedKeyPassphrase = 'secret'
const configurations = {
es: { 256: 'prime256v1', 384: 'secp384r1', 512: 'secp521r1' },
ppes: { 256: 'prime256v1', 384: 'secp384r1', 512: 'secp521r1' },
rs: { 512: null },
pprs: { 512: null },
ps: { 512: null },
Expand Down Expand Up @@ -40,7 +41,8 @@ for (const [prefix, configuration] of Object.entries(configurations)) {
}
} else {
for (const [bits, namedCurve] of Object.entries(configuration)) {
const isPasswordProtectedPrivateKey = prefix === 'pprs'
const isPasswordProtectedPrivateKey = prefix === 'pprs' || prefix === 'ppes'
const isEcAlgorithm = prefix === 'es' || prefix === 'ppes'
let type = 'pkcs8'
let format = 'pem'

Expand All @@ -53,7 +55,7 @@ for (const [prefix, configuration] of Object.entries(configurations)) {
}

generateKeyPair(
prefix === 'es' ? 'ec' : 'rsa',
isEcAlgorithm ? 'ec' : 'rsa',
{
modulusLength: 4096,
namedCurve,
Expand Down
13 changes: 12 additions & 1 deletion benchmarks/keys/generate-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@ const privateKeys = {
ES256: readFileSync(resolve(__dirname, './es-256-private.key')),
ES384: readFileSync(resolve(__dirname, './es-384-private.key')),
ES512: readFileSync(resolve(__dirname, './es-512-private.key')),
PPES384: readFileSync(resolve(__dirname, './ppes-384-private.key')),
PPES512: readFileSync(resolve(__dirname, './ppes-512-private.key')),
RS: readFileSync(resolve(__dirname, './rs-512-private.key')),
PPRS: readFileSync(resolve(__dirname, './pprs-512-private.key')),
PS: readFileSync(resolve(__dirname, './ps-512-private.key')),
EdDSA: readFileSync(resolve(__dirname, './ed-25519-private.key'))
}

for (const type of ['HS', 'ES', 'RS', 'PS']) {
for (let type of ['HS', 'ES', 'PPES', 'RS', 'PPRS', 'PS']) {
for (const bits of ['256', '384', '512']) {
if (type === 'PPES') {
type = 'ES'
}

if (type === 'PPRS') {
type = 'RS'
}

const algorithm = `${type}${bits}`
const key = privateKeys[type === 'ES' ? algorithm : type]

Expand Down
7 changes: 7 additions & 0 deletions benchmarks/keys/ppes-256-private.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAgXq9CvoCacNgICCAAw
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEIbhlSoyacanfdzTMJbYPTkEgZCj
GmxIs9JP00N6HQjP3FvG9ZiwW3/5UDEzAu8EnKRXmA+8oQ6R6XPDJ1G20GVWqHUh
jI60VPJmktQ8m7ask5HZWTl384uQmVPBk9T3xol0iVxeKCUEz3l7e/l/ZTnLul9f
hDZj/HU/QfQuIACgXVxCh+YXntT/GiFwhnd4DMeMj/LIJoopRREbK+1QfcefRbQ=
-----END ENCRYPTED PRIVATE KEY-----
4 changes: 4 additions & 0 deletions benchmarks/keys/ppes-256-public.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEalqaJjdigr3W/NfdvKADLIakgpIT
KulN0sG7lx8ReYSxRs9fMo2Y4ykJealAiV1wG6hAWRWp+pIomM1sPEEJ6A==
-----END PUBLIC KEY-----
8 changes: 8 additions & 0 deletions benchmarks/keys/ppes-384-private.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBHDBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIcxlDBJJy0x0CAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBXEQsFiMLXBEhLGdBSsDQVBIHA
j7SRL9TGv8XY0xyR2foNGcZxed5PqTw3iuXcWj5gA+jpa9t1gnqrIblAL7encKw1
7ktQLM9iHb3gdPr8S10ovuTGa8zYE51dItM8Zoo2ChW/Iy1tU6qlxY893gdI2Gxg
x/Lbru4Gh9K8uQL+d6NcX/73yx7gN4t3DYMHNX4mToB+nwdvDQ3ma0ISF4k9U0h2
bg34Sl9On+T6lHJoUfEwv+nz39Nt5dEd9+aVR2MpM4FrmkSLM3aUuLcGVQWgqrls
-----END ENCRYPTED PRIVATE KEY-----
5 changes: 5 additions & 0 deletions benchmarks/keys/ppes-384-public.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEaniWlb1Eznw6TytkNItMvYJ/ZnMkVWu3
I7NMSA6J2v7xT9jo5KX2dwY4k+KauLn4SAZjIVKtuO0Sn2oRkuBsvu9dnLayKB9J
AJs/dIpkw4gPwOXNmltRMHoeCrvEKBh1
-----END PUBLIC KEY-----
10 changes: 10 additions & 0 deletions benchmarks/keys/ppes-512-private.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBXTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIk6bDwPUm3VQCAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCfjMGBc8mX/KZfNN/FrhtkBIIB
ACCGQE5YEm5P5yuZ2P4jWTsVoYNXCTFbg946K4ACpTP5mrhOPIDiRUnXAd3eWLmR
aZPbcXiKXO8g/xp0jdfCleDvvqJn9X9OsRx6fnZeB8H3UXBPv19QVU4la9HxReSi
oiJf92345alvf6CofA/9ZgJ50ZpDwjG+1gzH1020ypOlmc6B3FDn52COaktTFKVw
9Oy4Akilnh2PCQ3Dgg8HHy1uouWiKeFygI6mB/rGhYUPCoW4X6RehrfN1YN/5pNM
PS9g/aMCr5i2fuC1cAml1p6vD1uuLu41eCSgUm+IS3UpjsL74L8atIj0/cCcghXu
fUnIG1TXK9LVRxcMAMlCvY8=
-----END ENCRYPTED PRIVATE KEY-----
6 changes: 6 additions & 0 deletions benchmarks/keys/ppes-512-public.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBDxNf8qJhavM8HkvjCpsW1EZw1wzR
TThYw3TfVPT4uU+710hBsI6g2wXx/EMYogqX9hf+BIVLREaEk7HUuy48S8IAc10X
6A7kOw3vcy0zkda28LkYC1YG3T1VySQrFSR/rBgqCTi9/kNGXME4InHOI5Lbfgj8
pcdHCw5a7uHyClEDnZw=
-----END PUBLIC KEY-----
23 changes: 12 additions & 11 deletions src/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function cacheSet(cache, key, value, error) {
return value || error
}

function performDetectPrivateKeyAlgoritm(key) {
function performDetectPrivateKeyAlgorithm(key) {
if (key.includes(publicKeyPemMatcher)) {
throw new TokenError(TokenError.codes.invalidKey, 'Public keys are not supported for signing.')
}
Expand All @@ -116,13 +116,14 @@ function performDetectPrivateKeyAlgoritm(key) {
let curveId

switch (pemData[1]) {
case 'RSA': // pkcs1 format - Can only be RSA or an ENCRYPTED (RSA) key
case 'ENCRYPTED':
case 'RSA': // pkcs1 format - Can only be RSA key
return 'RS256'
case 'EC': // sec1 format - Can only be a EC key
keyData = ECPrivateKey.decode(key, 'pem', { label: 'EC PRIVATE KEY' })
curveId = keyData.parameters.value.join('.')
break
case 'ENCRYPTED': // Can be either RSA or EC key - we'll used the supplied algorithm
return 'ENCRYPTED'
default:
// pkcs8
keyData = PrivateKey.decode(key, 'pem', { label: 'PRIVATE KEY' })
Expand Down Expand Up @@ -186,7 +187,7 @@ function performDetectPublicKeyAlgorithms(key) {
return [`ES${curve.bits}`]
}

function detectPrivateKeyAlgorithm(key) {
function detectPrivateKeyAlgorithm(key, providedAlgorithm) {
if (key instanceof Buffer) {
key = key.toString('utf-8')
} else if (typeof key !== 'string') {
Expand All @@ -204,14 +205,14 @@ function detectPrivateKeyAlgorithm(key) {

// Try detecting
try {
return cacheSet(privateKeysCache, key, performDetectPrivateKeyAlgoritm(key))
const detectedAlgorithm = performDetectPrivateKeyAlgorithm(key)

if (detectedAlgorithm === 'ENCRYPTED') {
return cacheSet(privateKeysCache, key, providedAlgorithm)
}
return cacheSet(privateKeysCache, key, detectedAlgorithm)
} catch (e) {
throw cacheSet(
privateKeysCache,
key,
null,
TokenError.wrap(e, TokenError.codes.invalidKey, 'Unsupported PEM private key.')
)
throw cacheSet(privateKeysCache, key, null, TokenError.wrap(e, TokenError.codes.invalidKey, 'Unsupported PEM private key.'))
}
}

Expand Down
25 changes: 18 additions & 7 deletions src/signer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ function checkIsCompatibleAlgorithm(expected, actual) {

let valid = true // We accept everything for HS

// If the key is passphrase encrypted (actual === "ENCRYPTED") only RS and ES algos are supported
if (expectedType === 'RS' || expectedType === 'PS') {
// RS and PS use same keys
valid = actualType === 'RS'
valid = actualType === 'RS' || (expectedType === 'RS' && actual === 'ENCRYPTED')
} else if (expectedType === 'ES' || expectedType === 'Ed') {
// ES and Ed must match
valid = expectedType === actualType
valid = expectedType === actualType || (expectedType === 'ES' && actual === 'ENCRYPTED')
}

if (!valid) {
Expand Down Expand Up @@ -144,7 +145,7 @@ function sign(
let token
try {
// Detect the private key - If the algorithm was known, just verify they match, otherwise assign it
const availableAlgorithm = detectPrivateKeyAlgorithm(currentKey)
const availableAlgorithm = detectPrivateKeyAlgorithm(currentKey, algorithm)

if (algorithm) {
checkIsCompatibleAlgorithm(algorithm, availableAlgorithm)
Expand Down Expand Up @@ -205,7 +206,7 @@ module.exports = function createSigner(options) {
}

const keyType = typeof key
const isKeyPasswordProtected = (keyType === 'object') && key && key.key && key.passphrase
const isKeyPasswordProtected = keyType === 'object' && key && key.key && key.passphrase

if (algorithm === 'none') {
if (key) {
Expand All @@ -214,17 +215,25 @@ module.exports = function createSigner(options) {
'The key option must not be provided when the algorithm option is "none".'
)
}
} else if (!key || (keyType !== 'string' && !(key instanceof Buffer) && keyType !== 'function' && !isKeyPasswordProtected)) {
} else if (
!key ||
(keyType !== 'string' && !(key instanceof Buffer) && keyType !== 'function' && !isKeyPasswordProtected)
) {
throw new TokenError(
TokenError.codes.invalidOption,
'The key option must be a string, a buffer, an object containing key/passphrase properties or a function returning the algorithm secret or private key.'
)
} else if (isKeyPasswordProtected && !algorithm) {
throw new TokenError(
TokenError.codes.invalidAlgorithm,
'When using password protected key you must provide the algorithm option.'
)
}

// Convert the key to a string when not a function, in order to be able to detect
if (key && keyType !== 'function') {
// Detect the private key - If the algorithm was known, just verify they match, otherwise assign it
const availableAlgorithm = detectPrivateKeyAlgorithm(isKeyPasswordProtected ? key.key : key)
const availableAlgorithm = detectPrivateKeyAlgorithm(isKeyPasswordProtected ? key.key : key, algorithm)

if (algorithm) {
checkIsCompatibleAlgorithm(algorithm, availableAlgorithm)
Expand Down Expand Up @@ -276,7 +285,9 @@ module.exports = function createSigner(options) {
}

const fpo = { jti, aud, iss, sub, nonce }
const fixedPayload = Object.keys(fpo).reduce((obj, key) => { return (fpo[key] !== undefined) ? Object.assign(obj, { [key]: fpo[key] }) : obj }, {})
const fixedPayload = Object.keys(fpo).reduce((obj, key) => {
return fpo[key] !== undefined ? Object.assign(obj, { [key]: fpo[key] }) : obj
}, {})

// Return the signer
const context = {
Expand Down
101 changes: 98 additions & 3 deletions test/signer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const privateKeys = {
ES256: readFileSync(resolve(__dirname, '../benchmarks/keys/es-256-private.key')),
ES384: readFileSync(resolve(__dirname, '../benchmarks/keys/es-384-private.key')),
ES512: readFileSync(resolve(__dirname, '../benchmarks/keys/es-512-private.key')),
PPES256: readFileSync(resolve(__dirname, '../benchmarks/keys/ppes-256-private.key')),
PPES384: readFileSync(resolve(__dirname, '../benchmarks/keys/ppes-384-private.key')),
PPES512: readFileSync(resolve(__dirname, '../benchmarks/keys/ppes-512-private.key')),
RS: readFileSync(resolve(__dirname, '../benchmarks/keys/rs-512-private.key')),
PPRS: readFileSync(resolve(__dirname, '../benchmarks/keys/pprs-512-private.key')),
PS: readFileSync(resolve(__dirname, '../benchmarks/keys/ps-512-private.key')),
Expand All @@ -24,6 +27,9 @@ const publicKeys = {
ES256: readFileSync(resolve(__dirname, '../benchmarks/keys/es-256-public.key')),
ES384: readFileSync(resolve(__dirname, '../benchmarks/keys/es-384-public.key')),
ES512: readFileSync(resolve(__dirname, '../benchmarks/keys/es-512-public.key')),
PPES256: readFileSync(resolve(__dirname, '../benchmarks/keys/ppes-256-public.key')),
PPES384: readFileSync(resolve(__dirname, '../benchmarks/keys/ppes-384-public.key')),
PPES512: readFileSync(resolve(__dirname, '../benchmarks/keys/ppes-512-public.key')),
RS: readFileSync(resolve(__dirname, '../benchmarks/keys/rs-512-public.key')),
PPRS: readFileSync(resolve(__dirname, '../benchmarks/keys/pprs-512-public.key')),
PS: readFileSync(resolve(__dirname, '../benchmarks/keys/ps-512-public.key')),
Expand Down Expand Up @@ -98,10 +104,10 @@ test('it correctly returns a token - callback - key as promise', t => {
})
})

test('it correctly returns a token - key as passphrase protected key', async t => {
test('it correctly returns a token - key as an RSA passphrase protected key', async t => {
const payload = { a: 1 }
if (useNewCrypto) {
const signedToken = sign(payload, { key: { key: privateKeys.PPRS, passphrase: 'secret' } })
const signedToken = sign(payload, { algorithm: 'RS256', key: { key: privateKeys.PPRS, passphrase: 'secret' } })
const decoder = createDecoder()
const result = decoder(signedToken)

Expand All @@ -116,6 +122,80 @@ test('it correctly returns a token - key as passphrase protected key', async t =
}
})

test('it correctly returns a token - key as an ES256 passphrase protected key', async t => {
const payload = { a: 1 }
if (useNewCrypto) {
const signedToken = sign(payload, { algorithm: 'ES256', key: { key: privateKeys.PPES256, passphrase: 'secret' } })
const decoder = createDecoder()
const result = decoder(signedToken)

t.equal(payload.a, result.a)
} else {
t.throws(() => sign(payload, { key: { key: privateKeys.PPES256, passphrase: 'secret' } }), {
message: 'Cannot create the signature.',
originalError: {
message: 'The "key" argument must be one of type string, Buffer, TypedArray, or DataView. Received type object'
}
})
}
})

test('it correctly returns a token - key as an ES384 passphrase protected key', async t => {
const payload = { a: 1 }
if (useNewCrypto) {
const signedToken = sign(payload, { algorithm: 'ES384', key: { key: privateKeys.PPES384, passphrase: 'secret' } })
const decoder = createDecoder()
const result = decoder(signedToken)

t.equal(payload.a, result.a)
} else {
t.throws(() => sign(payload, { key: { key: privateKeys.PPES384, passphrase: 'secret' } }), {
message: 'Cannot create the signature.',
originalError: {
message: 'The "key" argument must be one of type string, Buffer, TypedArray, or DataView. Received type object'
}
})
}
})

test('it correctly returns a token - key as an ES512 passphrase protected key', async t => {
const payload = { a: 1 }
if (useNewCrypto) {
const signedToken = sign(payload, { algorithm: 'ES512', key: { key: privateKeys.PPES512, passphrase: 'secret' } })
const decoder = createDecoder()
const result = decoder(signedToken)

t.equal(payload.a, result.a)
} else {
t.throws(() => sign(payload, { key: { key: privateKeys.PPES512, passphrase: 'secret' } }), {
message: 'Cannot create the signature.',
originalError: {
message: 'The "key" argument must be one of type string, Buffer, TypedArray, or DataView. Received type object'
}
})
}
})

test('it correctly returns an error when algorithm is not provided when using passphrase protected key', async t => {
t.throws(() => sign({ a: 1 }, { key: { key: privateKeys.PPRS, passphrase: 'secret' } }), {
message: 'When using password protected key you must provide the algorithm option.'
})
})

test('it correctly returns an error when using "EdDSA" algorithm passphrase protected key', async t => {
t.throws(() => sign({ a: 1 }, { algorithm: 'EdDSA', key: { key: privateKeys.PPRS, passphrase: 'secret' } }), {
message: 'Invalid private key provided for algorithm EdDSA.',
code: TokenError.codes.invalidKey
})
})

test('it correctly returns an error when using "ES256" algorithm with RSA private key', async t => {
t.throws(() => sign({ a: 1 }, { algorithm: 'ES256', key: privateKeys.RS }), {
message: 'Invalid private key provided for algorithm ES256.',
code: TokenError.codes.invalidKey
})
})

test('it correctly autodetects the algorithm depending on the secret provided', async t => {
const hsVerifier = createVerifier({ complete: true, key: publicKeys.HS })
const rsVerifier = createVerifier({ complete: true, key: publicKeys.RS })
Expand All @@ -124,6 +204,9 @@ test('it correctly autodetects the algorithm depending on the secret provided',
const es256Verifier = createVerifier({ complete: true, key: publicKeys.ES256 })
const es384Verifier = createVerifier({ complete: true, key: publicKeys.ES384 })
const es512Verifier = createVerifier({ complete: true, key: publicKeys.ES512 })
const ppes256Verifier = createVerifier({ complete: true, key: publicKeys.PPES256 })
const ppes384Verifier = createVerifier({ complete: true, key: publicKeys.PPES384 })
const ppes512Verifier = createVerifier({ complete: true, key: publicKeys.PPES512 })
const es25519Verifier = createVerifier({ complete: true, key: publicKeys.Ed25519 })
const es448Verifier = createVerifier({ complete: true, key: publicKeys.Ed448 })

Expand Down Expand Up @@ -152,10 +235,22 @@ test('it correctly autodetects the algorithm depending on the secret provided',
t.equal(verification.header.alg, 'ES512')

if (useNewCrypto) {
token = createSigner({ key: { key: privateKeys.PPRS, passphrase: 'secret' } })({ a: 1 })
token = createSigner({ algorithm: 'RS256', key: { key: privateKeys.PPRS, passphrase: 'secret' } })({ a: 1 })
verification = pprsVerifier(token)
t.equal(verification.header.alg, 'RS256')

token = createSigner({ algorithm: 'ES256', key: { key: privateKeys.PPES256, passphrase: 'secret' } })({ a: 1 })
verification = ppes256Verifier(token)
t.equal(verification.header.alg, 'ES256')

token = createSigner({ algorithm: 'ES384', key: { key: privateKeys.PPES384, passphrase: 'secret' } })({ a: 1 })
verification = ppes384Verifier(token)
t.equal(verification.header.alg, 'ES384')

token = createSigner({ algorithm: 'ES512', key: { key: privateKeys.PPES512, passphrase: 'secret' } })({ a: 1 })
verification = ppes512Verifier(token)
t.equal(verification.header.alg, 'ES512')

token = createSigner({ key: privateKeys.Ed25519 })({ a: 1 })
verification = es25519Verifier(token)
t.equal(verification.header.alg, 'EdDSA')
Expand Down

0 comments on commit 1ab4ce3

Please sign in to comment.