diff --git a/README.md b/README.md index 75d40e2..8aba6f2 100644 --- a/README.md +++ b/README.md @@ -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: '', diff --git a/benchmarks/keys/generate-keys.js b/benchmarks/keys/generate-keys.js index 0528dfd..927f811 100755 --- a/benchmarks/keys/generate-keys.js +++ b/benchmarks/keys/generate-keys.js @@ -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 }, @@ -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' @@ -53,7 +55,7 @@ for (const [prefix, configuration] of Object.entries(configurations)) { } generateKeyPair( - prefix === 'es' ? 'ec' : 'rsa', + isEcAlgorithm ? 'ec' : 'rsa', { modulusLength: 4096, namedCurve, diff --git a/benchmarks/keys/generate-tokens.js b/benchmarks/keys/generate-tokens.js index 22048ab..726b180 100644 --- a/benchmarks/keys/generate-tokens.js +++ b/benchmarks/keys/generate-tokens.js @@ -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] diff --git a/benchmarks/keys/ppes-256-private.key b/benchmarks/keys/ppes-256-private.key new file mode 100644 index 0000000..62ed2b8 --- /dev/null +++ b/benchmarks/keys/ppes-256-private.key @@ -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----- diff --git a/benchmarks/keys/ppes-256-public.key b/benchmarks/keys/ppes-256-public.key new file mode 100644 index 0000000..e8aacc0 --- /dev/null +++ b/benchmarks/keys/ppes-256-public.key @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEalqaJjdigr3W/NfdvKADLIakgpIT +KulN0sG7lx8ReYSxRs9fMo2Y4ykJealAiV1wG6hAWRWp+pIomM1sPEEJ6A== +-----END PUBLIC KEY----- diff --git a/benchmarks/keys/ppes-384-private.key b/benchmarks/keys/ppes-384-private.key new file mode 100644 index 0000000..0ab3608 --- /dev/null +++ b/benchmarks/keys/ppes-384-private.key @@ -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----- diff --git a/benchmarks/keys/ppes-384-public.key b/benchmarks/keys/ppes-384-public.key new file mode 100644 index 0000000..35ada26 --- /dev/null +++ b/benchmarks/keys/ppes-384-public.key @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEaniWlb1Eznw6TytkNItMvYJ/ZnMkVWu3 +I7NMSA6J2v7xT9jo5KX2dwY4k+KauLn4SAZjIVKtuO0Sn2oRkuBsvu9dnLayKB9J +AJs/dIpkw4gPwOXNmltRMHoeCrvEKBh1 +-----END PUBLIC KEY----- diff --git a/benchmarks/keys/ppes-512-private.key b/benchmarks/keys/ppes-512-private.key new file mode 100644 index 0000000..7d8761e --- /dev/null +++ b/benchmarks/keys/ppes-512-private.key @@ -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----- diff --git a/benchmarks/keys/ppes-512-public.key b/benchmarks/keys/ppes-512-public.key new file mode 100644 index 0000000..ffafb51 --- /dev/null +++ b/benchmarks/keys/ppes-512-public.key @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBDxNf8qJhavM8HkvjCpsW1EZw1wzR +TThYw3TfVPT4uU+710hBsI6g2wXx/EMYogqX9hf+BIVLREaEk7HUuy48S8IAc10X +6A7kOw3vcy0zkda28LkYC1YG3T1VySQrFSR/rBgqCTi9/kNGXME4InHOI5Lbfgj8 +pcdHCw5a7uHyClEDnZw= +-----END PUBLIC KEY----- diff --git a/src/crypto.js b/src/crypto.js index 0302ed3..f0fa2d0 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -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.') } @@ -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' }) @@ -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') { @@ -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.')) } } diff --git a/src/signer.js b/src/signer.js index e0b6ab7..8e8659f 100644 --- a/src/signer.js +++ b/src/signer.js @@ -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) { @@ -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) @@ -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) { @@ -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) @@ -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 = { diff --git a/test/signer.spec.js b/test/signer.spec.js index b524c08..52fee4b 100644 --- a/test/signer.spec.js +++ b/test/signer.spec.js @@ -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')), @@ -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')), @@ -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) @@ -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 }) @@ -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 }) @@ -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')