From 0e710aada401b1cf89b284d8469d112ddf277fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Mon, 6 Mar 2017 00:41:26 +0100 Subject: [PATCH] crypto: add sign/verify support for RSASSA-PSS Adds support for the PSS padding scheme. Until now, the sign/verify functions used the old EVP_Sign*/EVP_Verify* OpenSSL API, making it impossible to change the padding scheme. Fixed by first computing the message digest and then signing/verifying with a custom EVP_PKEY_CTX, allowing us to specify options such as the padding scheme and the PSS salt length. Fixes: https://github.com/nodejs/node/issues/1127 PR-URL: https://github.com/nodejs/node/pull/11705 Reviewed-By: Shigeki Ohtsu Reviewed-By: Sam Roberts Reviewed-By: Ben Noordhuis Reviewed-By: Anna Henningsen --- doc/api/crypto.md | 60 ++++++- lib/crypto.js | 49 +++++- src/node_constants.cc | 12 ++ src/node_constants.h | 13 ++ src/node_crypto.cc | 123 ++++++++++++-- src/node_crypto.h | 6 +- test/fixtures/pss-vectors.json | 89 ++++++++++ test/parallel/test-crypto-sign-verify.js | 197 +++++++++++++++++++++++ 8 files changed, 530 insertions(+), 19 deletions(-) create mode 100644 test/fixtures/pss-vectors.json diff --git a/doc/api/crypto.md b/doc/api/crypto.md index f3584cb5a530a1..fcbdcb6bfcb621 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -964,6 +964,10 @@ console.log(sign.sign(privateKey).toString('hex')); ### sign.sign(private_key[, output_format]) - `private_key` {string | Object} - `key` {string} @@ -975,10 +979,21 @@ Calculates the signature on all the data passed through using either The `private_key` argument can be an object or a string. If `private_key` is a string, it is treated as a raw key with no passphrase. If `private_key` is an -object, it is interpreted as a hash containing two properties: +object, it must contain one or more of the following properties: -* `key`: {string} - PEM encoded private key +* `key`: {string} - PEM encoded private key (required) * `passphrase`: {string} - passphrase for the private key +* `padding`: {integer} - Optional padding value for RSA, one of the following: + * `crypto.constants.RSA_PKCS1_PADDING` (default) + * `crypto.constants.RSA_PKCS1_PSS_PADDING` + + Note that `RSA_PKCS1_PSS_PADDING` will use MGF1 with the same hash function + used to sign the message as specified in section 3.1 of [RFC 4055][]. +* `saltLength`: {integer} - salt length for when padding is + `RSA_PKCS1_PSS_PADDING`. The special value + `crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest + size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the + maximum permissible value. The `output_format` can specify one of `'latin1'`, `'hex'` or `'base64'`. If `output_format` is provided a string is returned; otherwise a [`Buffer`][] is @@ -1073,14 +1088,33 @@ This can be called many times with new data as it is streamed. ### verifier.verify(object, signature[, signature_format]) -- `object` {string} +- `object` {string | Object} - `signature` {string | Buffer | Uint8Array} - `signature_format` {string} Verifies the provided data using the given `object` and `signature`. -The `object` argument is a string containing a PEM encoded object, which can be -an RSA public key, a DSA public key, or an X.509 certificate. +The `object` argument can be either a string containing a PEM encoded object, +which can be an RSA public key, a DSA public key, or an X.509 certificate, +or an object with one or more of the following properties: + +* `key`: {string} - PEM encoded private key (required) +* `padding`: {integer} - Optional padding value for RSA, one of the following: + * `crypto.constants.RSA_PKCS1_PADDING` (default) + * `crypto.constants.RSA_PKCS1_PSS_PADDING` + + Note that `RSA_PKCS1_PSS_PADDING` will use MGF1 with the same hash function + used to verify the message as specified in section 3.1 of [RFC 4055][]. +* `saltLength`: {integer} - salt length for when padding is + `RSA_PKCS1_PSS_PADDING`. The special value + `crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest + size, `crypto.constants.RSA_PSS_SALTLEN_AUTO` (default) causes it to be + determined automatically. + The `signature` argument is the previously calculated signature for the data, in the `signature_format` which can be `'latin1'`, `'hex'` or `'base64'`. If a `signature_format` is specified, the `signature` is expected to be a @@ -2047,6 +2081,21 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL. RSA_PKCS1_PSS_PADDING + + RSA_PSS_SALTLEN_DIGEST + Sets the salt length for `RSA_PKCS1_PSS_PADDING` to the digest size + when signing or verifying. + + + RSA_PSS_SALTLEN_MAX_SIGN + Sets the salt length for `RSA_PKCS1_PSS_PADDING` to the maximum + permissible value when signing data. + + + RSA_PSS_SALTLEN_AUTO + Causes the salt length for `RSA_PKCS1_PSS_PADDING` to be determined + automatically when verifying a signature. + POINT_CONVERSION_COMPRESSED @@ -2122,6 +2171,7 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL. [publicly trusted list of CAs]: https://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt [RFC 2412]: https://www.rfc-editor.org/rfc/rfc2412.txt [RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt +[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt [stream]: stream.html [stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback [Crypto Constants]: #crypto_crypto_constants_1 diff --git a/lib/crypto.js b/lib/crypto.js index 662ddef60ef176..3e7ed5e9c86960 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -304,7 +304,28 @@ Sign.prototype.sign = function sign(options, encoding) { var key = options.key || options; var passphrase = options.passphrase || null; - var ret = this._handle.sign(toBuf(key), null, passphrase); + + // Options specific to RSA + var rsaPadding = constants.RSA_PKCS1_PADDING; + if (options.hasOwnProperty('padding')) { + if (options.padding === options.padding >> 0) { + rsaPadding = options.padding; + } else { + throw new TypeError('padding must be an integer'); + } + } + + var pssSaltLength = constants.RSA_PSS_SALTLEN_AUTO; + if (options.hasOwnProperty('saltLength')) { + if (options.saltLength === options.saltLength >> 0) { + pssSaltLength = options.saltLength; + } else { + throw new TypeError('saltLength must be an integer'); + } + } + + var ret = this._handle.sign(toBuf(key), null, passphrase, rsaPadding, + pssSaltLength); encoding = encoding || exports.DEFAULT_ENCODING; if (encoding && encoding !== 'buffer') @@ -330,9 +351,31 @@ util.inherits(Verify, stream.Writable); Verify.prototype._write = Sign.prototype._write; Verify.prototype.update = Sign.prototype.update; -Verify.prototype.verify = function verify(object, signature, sigEncoding) { +Verify.prototype.verify = function verify(options, signature, sigEncoding) { + var key = options.key || options; sigEncoding = sigEncoding || exports.DEFAULT_ENCODING; - return this._handle.verify(toBuf(object), toBuf(signature, sigEncoding)); + + // Options specific to RSA + var rsaPadding = constants.RSA_PKCS1_PADDING; + if (options.hasOwnProperty('padding')) { + if (options.padding === options.padding >> 0) { + rsaPadding = options.padding; + } else { + throw new TypeError('padding must be an integer'); + } + } + + var pssSaltLength = constants.RSA_PSS_SALTLEN_AUTO; + if (options.hasOwnProperty('saltLength')) { + if (options.saltLength === options.saltLength >> 0) { + pssSaltLength = options.saltLength; + } else { + throw new TypeError('saltLength must be an integer'); + } + } + + return this._handle.verify(toBuf(key), toBuf(signature, sigEncoding), null, + rsaPadding, pssSaltLength); }; function rsaPublic(method, defaultPadding) { diff --git a/src/node_constants.cc b/src/node_constants.cc index 5bde53fcdf1ae5..8bc95392f26bcf 100644 --- a/src/node_constants.cc +++ b/src/node_constants.cc @@ -997,6 +997,18 @@ void DefineOpenSSLConstants(Local target) { NODE_DEFINE_CONSTANT(target, RSA_PKCS1_PSS_PADDING); #endif +#ifdef RSA_PSS_SALTLEN_DIGEST + NODE_DEFINE_CONSTANT(target, RSA_PSS_SALTLEN_DIGEST); +#endif + +#ifdef RSA_PSS_SALTLEN_MAX_SIGN + NODE_DEFINE_CONSTANT(target, RSA_PSS_SALTLEN_MAX_SIGN); +#endif + +#ifdef RSA_PSS_SALTLEN_AUTO + NODE_DEFINE_CONSTANT(target, RSA_PSS_SALTLEN_AUTO); +#endif + #if HAVE_OPENSSL // NOTE: These are not defines NODE_DEFINE_CONSTANT(target, POINT_CONVERSION_COMPRESSED); diff --git a/src/node_constants.h b/src/node_constants.h index 047d8fc5e7e2f5..1de420e2def571 100644 --- a/src/node_constants.h +++ b/src/node_constants.h @@ -28,6 +28,19 @@ #include "v8.h" #if HAVE_OPENSSL + +#ifndef RSA_PSS_SALTLEN_DIGEST +#define RSA_PSS_SALTLEN_DIGEST -1 +#endif + +#ifndef RSA_PSS_SALTLEN_MAX_SIGN +#define RSA_PSS_SALTLEN_MAX_SIGN -2 +#endif + +#ifndef RSA_PSS_SALTLEN_AUTO +#define RSA_PSS_SALTLEN_AUTO -2 +#endif + #define DEFAULT_CIPHER_LIST_CORE "ECDHE-RSA-AES128-GCM-SHA256:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:" \ "ECDHE-RSA-AES256-GCM-SHA384:" \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 5d93184ba65b9b..45b06eaff50385 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -21,6 +21,7 @@ #include "node.h" #include "node_buffer.h" +#include "node_constants.h" #include "node_crypto.h" #include "node_crypto_bio.h" #include "node_crypto_groups.h" @@ -101,6 +102,7 @@ using v8::HandleScope; using v8::Integer; using v8::Isolate; using v8::Local; +using v8::Maybe; using v8::Null; using v8::Object; using v8::Persistent; @@ -3976,6 +3978,19 @@ void SignBase::CheckThrow(SignBase::Error error) { } } +static bool ApplyRSAOptions(EVP_PKEY* pkey, EVP_PKEY_CTX* pkctx, int padding, + int salt_len) { + if (pkey->type == EVP_PKEY_RSA || pkey->type == EVP_PKEY_RSA2) { + if (EVP_PKEY_CTX_set_rsa_padding(pkctx, padding) <= 0) + return false; + if (padding == RSA_PKCS1_PSS_PADDING) { + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkctx, salt_len) <= 0) + return false; + } + } + + return true; +} @@ -4005,7 +4020,7 @@ SignBase::Error Sign::SignInit(const char* sign_type) { return kSignUnknownDigest; EVP_MD_CTX_init(&mdctx_); - if (!EVP_SignInit_ex(&mdctx_, md, nullptr)) + if (!EVP_DigestInit_ex(&mdctx_, md, nullptr)) return kSignInit; initialised_ = true; @@ -4032,7 +4047,7 @@ void Sign::SignInit(const FunctionCallbackInfo& args) { SignBase::Error Sign::SignUpdate(const char* data, int len) { if (!initialised_) return kSignNotInitialised; - if (!EVP_SignUpdate(&mdctx_, data, len)) + if (!EVP_DigestUpdate(&mdctx_, data, len)) return kSignUpdate; return kSignOk; } @@ -4062,12 +4077,54 @@ void Sign::SignUpdate(const FunctionCallbackInfo& args) { sign->CheckThrow(err); } +static int Node_SignFinal(EVP_MD_CTX* mdctx, unsigned char* md, + unsigned int* sig_len, EVP_PKEY* pkey, int padding, + int pss_salt_len) { + unsigned char m[EVP_MAX_MD_SIZE]; + unsigned int m_len; + int rv = 0; + EVP_PKEY_CTX* pkctx = nullptr; + + *sig_len = 0; + if (!EVP_DigestFinal_ex(mdctx, m, &m_len)) + return rv; + + if (mdctx->digest->flags & EVP_MD_FLAG_PKEY_METHOD_SIGNATURE) { + size_t sltmp = static_cast(EVP_PKEY_size(pkey)); + pkctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (pkctx == nullptr) + goto err; + if (EVP_PKEY_sign_init(pkctx) <= 0) + goto err; + if (!ApplyRSAOptions(pkey, pkctx, padding, pss_salt_len)) + goto err; + if (EVP_PKEY_CTX_set_signature_md(pkctx, mdctx->digest) <= 0) + goto err; + if (EVP_PKEY_sign(pkctx, md, &sltmp, m, m_len) <= 0) + goto err; + *sig_len = sltmp; + rv = 1; + err: + EVP_PKEY_CTX_free(pkctx); + return rv; + } + + if (mdctx->digest->sign == nullptr) { + EVPerr(EVP_F_EVP_SIGNFINAL, EVP_R_NO_SIGN_FUNCTION_CONFIGURED); + return 0; + } + + return mdctx->digest->sign(mdctx->digest->type, m, m_len, md, sig_len, + pkey->pkey.ptr); +} SignBase::Error Sign::SignFinal(const char* key_pem, int key_pem_len, const char* passphrase, unsigned char** sig, - unsigned int *sig_len) { + unsigned int* sig_len, + int padding, + int salt_len) { if (!initialised_) return kSignNotInitialised; @@ -4113,7 +4170,7 @@ SignBase::Error Sign::SignFinal(const char* key_pem, } #endif // NODE_FIPS_MODE - if (EVP_SignFinal(&mdctx_, *sig, sig_len, pkey)) + if (Node_SignFinal(&mdctx_, *sig, sig_len, pkey, padding, salt_len)) fatal = false; initialised_ = false; @@ -4156,6 +4213,16 @@ void Sign::SignFinal(const FunctionCallbackInfo& args) { size_t buf_len = Buffer::Length(args[0]); char* buf = Buffer::Data(args[0]); + CHECK(args[3]->IsInt32()); + Maybe maybe_padding = args[3]->Int32Value(env->context()); + CHECK(maybe_padding.IsJust()); + int padding = maybe_padding.ToChecked(); + + CHECK(args[4]->IsInt32()); + Maybe maybe_salt_len = args[4]->Int32Value(env->context()); + CHECK(maybe_salt_len.IsJust()); + int salt_len = maybe_salt_len.ToChecked(); + md_len = 8192; // Maximum key size is 8192 bits md_value = new unsigned char[md_len]; @@ -4167,7 +4234,9 @@ void Sign::SignFinal(const FunctionCallbackInfo& args) { buf_len, len >= 3 && !args[2]->IsNull() ? *passphrase : nullptr, &md_value, - &md_len); + &md_len, + padding, + salt_len); if (err != kSignOk) { delete[] md_value; md_value = nullptr; @@ -4211,7 +4280,7 @@ SignBase::Error Verify::VerifyInit(const char* verify_type) { return kSignUnknownDigest; EVP_MD_CTX_init(&mdctx_); - if (!EVP_VerifyInit_ex(&mdctx_, md, nullptr)) + if (!EVP_DigestInit_ex(&mdctx_, md, nullptr)) return kSignInit; initialised_ = true; @@ -4239,7 +4308,7 @@ SignBase::Error Verify::VerifyUpdate(const char* data, int len) { if (!initialised_) return kSignNotInitialised; - if (!EVP_VerifyUpdate(&mdctx_, data, len)) + if (!EVP_DigestUpdate(&mdctx_, data, len)) return kSignUpdate; return kSignOk; @@ -4275,6 +4344,8 @@ SignBase::Error Verify::VerifyFinal(const char* key_pem, int key_pem_len, const char* sig, int siglen, + int padding, + int saltlen, bool* verify_result) { if (!initialised_) return kSignNotInitialised; @@ -4286,7 +4357,10 @@ SignBase::Error Verify::VerifyFinal(const char* key_pem, BIO* bp = nullptr; X509* x509 = nullptr; bool fatal = true; + unsigned char m[EVP_MAX_MD_SIZE]; + unsigned int m_len; int r = 0; + EVP_PKEY_CTX* pkctx = nullptr; bp = BIO_new_mem_buf(const_cast(key_pem), key_pem_len); if (bp == nullptr) @@ -4321,11 +4395,29 @@ SignBase::Error Verify::VerifyFinal(const char* key_pem, goto exit; } + if (!EVP_DigestFinal_ex(&mdctx_, m, &m_len)) { + goto exit; + } + fatal = false; - r = EVP_VerifyFinal(&mdctx_, + + pkctx = EVP_PKEY_CTX_new(pkey, nullptr); + if (pkctx == nullptr) + goto err; + if (EVP_PKEY_verify_init(pkctx) <= 0) + goto err; + if (!ApplyRSAOptions(pkey, pkctx, padding, saltlen)) + goto err; + if (EVP_PKEY_CTX_set_signature_md(pkctx, mdctx_.digest) <= 0) + goto err; + r = EVP_PKEY_verify(pkctx, reinterpret_cast(sig), siglen, - pkey); + m, + m_len); + + err: + EVP_PKEY_CTX_free(pkctx); exit: if (pkey != nullptr) @@ -4381,8 +4473,19 @@ void Verify::VerifyFinal(const FunctionCallbackInfo& args) { hbuf = Buffer::Data(args[1]); } + CHECK(args[3]->IsInt32()); + Maybe maybe_padding = args[3]->Int32Value(env->context()); + CHECK(maybe_padding.IsJust()); + int padding = maybe_padding.ToChecked(); + + CHECK(args[4]->IsInt32()); + Maybe maybe_salt_len = args[4]->Int32Value(env->context()); + CHECK(maybe_salt_len.IsJust()); + int salt_len = maybe_salt_len.ToChecked(); + bool verify_result; - Error err = verify->VerifyFinal(kbuf, klen, hbuf, hlen, &verify_result); + Error err = verify->VerifyFinal(kbuf, klen, hbuf, hlen, padding, salt_len, + &verify_result); if (args[1]->IsString()) delete[] hbuf; if (err != kSignOk) diff --git a/src/node_crypto.h b/src/node_crypto.h index 63e6ab684fe2e4..ffb8444ce60145 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -592,7 +592,9 @@ class Sign : public SignBase { int key_pem_len, const char* passphrase, unsigned char** sig, - unsigned int *sig_len); + unsigned int *sig_len, + int padding, + int saltlen); protected: static void New(const v8::FunctionCallbackInfo& args); @@ -615,6 +617,8 @@ class Verify : public SignBase { int key_pem_len, const char* sig, int siglen, + int padding, + int saltlen, bool* verify_result); protected: diff --git a/test/fixtures/pss-vectors.json b/test/fixtures/pss-vectors.json new file mode 100644 index 00000000000000..b540d13a540646 --- /dev/null +++ b/test/fixtures/pss-vectors.json @@ -0,0 +1,89 @@ +{ + "example01": { + "publicKey": [ + "-----BEGIN PUBLIC KEY-----", + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQClbkoOcBAXWJpRh9x+qEHRVvLs", + "DjatUqRN/rHmH3rZkdjFEFb/7bFitMDyg6EqiKOU3/Umq3KRy7MHzqv84LHf1c2V", + "CAltWyuLbfXWce9jd8CSHLI8Jwpw4lmOb/idGfEFrMLT8Ms18pKA4Thrb2TE7yLh", + "4fINDOjP+yJJvZohNwIDAQAB", + "-----END PUBLIC KEY-----" + ], + "tests": [ + { + "message": "cdc87da223d786df3b45e0bbbc721326d1ee2af806cc315475cc6f0d9c66e1b62371d45ce2392e1ac92844c310102f156a0d8d52c1f4c40ba3aa65095786cb769757a6563ba958fed0bcc984e8b517a3d5f515b23b8a41e74aa867693f90dfb061a6e86dfaaee64472c00e5f20945729cbebe77f06ce78e08f4098fba41f9d6193c0317e8b60d4b6084acb42d29e3808a3bc372d85e331170fcbf7cc72d0b71c296648b3a4d10f416295d0807aa625cab2744fd9ea8fd223c42537029828bd16be02546f130fd2e33b936d2676e08aed1b73318b750a0167d0", + "salt": "dee959c7e06411361420ff80185ed57f3e6776af", + "signature": "9074308fb598e9701b2294388e52f971faac2b60a5145af185df5287b5ed2887e57ce7fd44dc8634e407c8e0e4360bc226f3ec227f9d9e54638e8d31f5051215df6ebb9c2f9579aa77598a38f914b5b9c1bd83c4e2f9f382a0d0aa3542ffee65984a601bc69eb28deb27dca12c82c2d4c3f66cd500f1ff2b994d8a4e30cbb33c" + }, + { + "message": "851384cdfe819c22ed6c4ccb30daeb5cf059bc8e1166b7e3530c4c233e2b5f8f71a1cca582d43ecc72b1bca16dfc7013226b9e", + "salt": "ef2869fa40c346cb183dab3d7bffc98fd56df42d", + "signature": "3ef7f46e831bf92b32274142a585ffcefbdca7b32ae90d10fb0f0c729984f04ef29a9df0780775ce43739b97838390db0a5505e63de927028d9d29b219ca2c4517832558a55d694a6d25b9dab66003c4cccd907802193be5170d26147d37b93590241be51c25055f47ef62752cfbe21418fafe98c22c4d4d47724fdb5669e843" + }, + { + "message": "a4b159941761c40c6a82f2b80d1b94f5aa2654fd17e12d588864679b54cd04ef8bd03012be8dc37f4b83af7963faff0dfa225477437c48017ff2be8191cf3955fc07356eab3f322f7f620e21d254e5db4324279fe067e0910e2e81ca2cab31c745e67a54058eb50d993cdb9ed0b4d029c06d21a94ca661c3ce27fae1d6cb20f4564d66ce4767583d0e5f060215b59017be85ea848939127bd8c9c4d47b51056c031cf336f17c9980f3b8f5b9b6878e8b797aa43b882684333e17893fe9caa6aa299f7ed1a18ee2c54864b7b2b99b72618fb02574d139ef50f019c9eef416971338e7d470", + "salt": "710b9c4747d800d4de87f12afdce6df18107cc77", + "signature": "666026fba71bd3e7cf13157cc2c51a8e4aa684af9778f91849f34335d141c00154c4197621f9624a675b5abc22ee7d5baaffaae1c9baca2cc373b3f33e78e6143c395a91aa7faca664eb733afd14d8827259d99a7550faca501ef2b04e33c23aa51f4b9e8282efdb728cc0ab09405a91607c6369961bc8270d2d4f39fce612b1" + }, + { + "message": "bc656747fa9eafb3f0", + "salt": "056f00985de14d8ef5cea9e82f8c27bef720335e", + "signature": "4609793b23e9d09362dc21bb47da0b4f3a7622649a47d464019b9aeafe53359c178c91cd58ba6bcb78be0346a7bc637f4b873d4bab38ee661f199634c547a1ad8442e03da015b136e543f7ab07c0c13e4225b8de8cce25d4f6eb8400f81f7e1833b7ee6e334d370964ca79fdb872b4d75223b5eeb08101591fb532d155a6de87" + }, + { + "message": "b45581547e5427770c768e8b82b75564e0ea4e9c32594d6bff706544de0a8776c7a80b4576550eee1b2acabc7e8b7d3ef7bb5b03e462c11047eadd00629ae575480ac1470fe046f13a2bf5af17921dc4b0aa8b02bee6334911651d7f8525d10f32b51d33be520d3ddf5a709955a3dfe78283b9e0ab54046d150c177f037fdccc5be4ea5f68b5e5a38c9d7edcccc4975f455a6909b4", + "salt": "80e70ff86a08de3ec60972b39b4fbfdcea67ae8e", + "signature": "1d2aad221ca4d31ddf13509239019398e3d14b32dc34dc5af4aeaea3c095af73479cf0a45e5629635a53a018377615b16cb9b13b3e09d671eb71e387b8545c5960da5a64776e768e82b2c93583bf104c3fdb23512b7b4e89f633dd0063a530db4524b01c3f384c09310e315a79dcd3d684022a7f31c865a664e316978b759fad" + }, + { + "message": "10aae9a0ab0b595d0841207b700d48d75faedde3b775cd6b4cc88ae06e4694ec74ba18f8520d4f5ea69cbbe7cc2beba43efdc10215ac4eb32dc302a1f53dc6c4352267e7936cfebf7c8d67035784a3909fa859c7b7b59b8e39c5c2349f1886b705a30267d402f7486ab4f58cad5d69adb17ab8cd0ce1caf5025af4ae24b1fb8794c6070cc09a51e2f9911311e3877d0044c71c57a993395008806b723ac38373d395481818528c1e7053739282053529510e935cd0fa77b8fa53cc2d474bd4fb3cc5c672d6ffdc90a00f9848712c4bcfe46c60573659b11e6457e861f0f604b6138d144f8ce4e2da73", + "salt": "a8ab69dd801f0074c2a1fc60649836c616d99681", + "signature": "2a34f6125e1f6b0bf971e84fbd41c632be8f2c2ace7de8b6926e31ff93e9af987fbc06e51e9be14f5198f91f3f953bd67da60a9df59764c3dc0fe08e1cbef0b75f868d10ad3fba749fef59fb6dac46a0d6e504369331586f58e4628f39aa278982543bc0eeb537dc61958019b394fb273f215858a0a01ac4d650b955c67f4c58" + } + ] + }, + "example10": { + "publicKey": [ + "-----BEGIN PUBLIC KEY-----", + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApd2GesTLAvkLlFfUjBSn", + "cO+ZHFbDnA7GX9Ea+ok3zqV7m+esc7RcABdhW4LWIuMYdTtgJ8D9FXvhL4CQ/uKn", + "rc0O73WfiLpJl8ekLVjJqhLLma4AH+UhwTu1QxRFqNWuT15MfpSKwifTYEBx8g5X", + "fpBfvrFd+vBtHeWuYlPWOmohILMaXaXavJVQYA4g8n03OeJieSX+o8xQnyHf8E5u", + "6kVJxUDWgJ/5MH7t6R//WHM9g4WiN9bTcFoz45GQCZIHDfet8TV89+NwDONmfeg/", + "F7jfF3jbOB3OCctK0FilEQAac4GY7ifPVaE7dUU5kGWC7IsXS9WNXR89dnxhNyGu", + "BQIDAQAB", + "-----END PUBLIC KEY-----" + ], + "tests": [ + { + "message": "883177e5126b9be2d9a9680327d5370c6f26861f5820c43da67a3ad609", + "salt": "04e215ee6ff934b9da70d7730c8734abfcecde89", + "signature": "82c2b160093b8aa3c0f7522b19f87354066c77847abf2a9fce542d0e84e920c5afb49ffdfdace16560ee94a1369601148ebad7a0e151cf16331791a5727d05f21e74e7eb811440206935d744765a15e79f015cb66c532c87a6a05961c8bfad741a9a6657022894393e7223739796c02a77455d0f555b0ec01ddf259b6207fd0fd57614cef1a5573baaff4ec00069951659b85f24300a25160ca8522dc6e6727e57d019d7e63629b8fe5e89e25cc15beb3a647577559299280b9b28f79b0409000be25bbd96408ba3b43cc486184dd1c8e62553fa1af4040f60663de7f5e49c04388e257f1ce89c95dab48a315d9b66b1b7628233876ff2385230d070d07e1666" + }, + { + "message": "dd670a01465868adc93f26131957a50c52fb777cdbaa30892c9e12361164ec13979d43048118e4445db87bee58dd987b3425d02071d8dbae80708b039dbb64dbd1de5657d9fed0c118a54143742e0ff3c87f74e45857647af3f79eb0a14c9d75ea9a1a04b7cf478a897a708fd988f48e801edb0b7039df8c23bb3c56f4e821ac", + "salt": "8b2bdd4b40faf545c778ddf9bc1a49cb57f9b71b", + "signature": "14ae35d9dd06ba92f7f3b897978aed7cd4bf5ff0b585a40bd46ce1b42cd2703053bb9044d64e813d8f96db2dd7007d10118f6f8f8496097ad75e1ff692341b2892ad55a633a1c55e7f0a0ad59a0e203a5b8278aec54dd8622e2831d87174f8caff43ee6c46445345d84a59659bfb92ecd4c818668695f34706f66828a89959637f2bf3e3251c24bdba4d4b7649da0022218b119c84e79a6527ec5b8a5f861c159952e23ec05e1e717346faefe8b1686825bd2b262fb2531066c0de09acde2e4231690728b5d85e115a2f6b92b79c25abc9bd9399ff8bcf825a52ea1f56ea76dd26f43baafa18bfa92a504cbd35699e26d1dcc5a2887385f3c63232f06f3244c3" + }, + { + "message": "48b2b6a57a63c84cea859d65c668284b08d96bdcaabe252db0e4a96cb1bac6019341db6fbefb8d106b0e90eda6bcc6c6262f37e7ea9c7e5d226bd7df85ec5e71efff2f54c5db577ff729ff91b842491de2741d0c631607df586b905b23b91af13da12304bf83eca8a73e871ff9db", + "salt": "4e96fc1b398f92b44671010c0dc3efd6e20c2d73", + "signature": "6e3e4d7b6b15d2fb46013b8900aa5bbb3939cf2c095717987042026ee62c74c54cffd5d7d57efbbf950a0f5c574fa09d3fc1c9f513b05b4ff50dd8df7edfa20102854c35e592180119a70ce5b085182aa02d9ea2aa90d1df03f2daae885ba2f5d05afdac97476f06b93b5bc94a1a80aa9116c4d615f333b098892b25fface266f5db5a5a3bcc10a824ed55aad35b727834fb8c07da28fcf416a5d9b2224f1f8b442b36f91e456fdea2d7cfe3367268de0307a4c74e924159ed33393d5e0655531c77327b89821bdedf880161c78cd4196b5419f7acc3f13e5ebf161b6e7c6724716ca33b85c2e25640192ac2859651d50bde7eb976e51cec828b98b6563b86bb" + }, + { + "message": "0b8777c7f839baf0a64bbbdbc5ce79755c57a205b845c174e2d2e90546a089c4e6ec8adffa23a7ea97bae6b65d782b82db5d2b5a56d22a29a05e7c4433e2b82a621abba90add05ce393fc48a840542451a", + "salt": "c7cd698d84b65128d8835e3a8b1eb0e01cb541ec", + "signature": "34047ff96c4dc0dc90b2d4ff59a1a361a4754b255d2ee0af7d8bf87c9bc9e7ddeede33934c63ca1c0e3d262cb145ef932a1f2c0a997aa6a34f8eaee7477d82ccf09095a6b8acad38d4eec9fb7eab7ad02da1d11d8e54c1825e55bf58c2a23234b902be124f9e9038a8f68fa45dab72f66e0945bf1d8bacc9044c6f07098c9fcec58a3aab100c805178155f030a124c450e5acbda47d0e4f10b80a23f803e774d023b0015c20b9f9bbe7c91296338d5ecb471cafb032007b67a60be5f69504a9f01abb3cb467b260e2bce860be8d95bf92c0c8e1496ed1e528593a4abb6df462dde8a0968dffe4683116857a232f5ebf6c85be238745ad0f38f767a5fdbf486fb" + }, + { + "message": "f1036e008e71e964dadc9219ed30e17f06b4b68a955c16b312b1eddf028b74976bed6b3f6a63d4e77859243c9cccdc98016523abb02483b35591c33aad81213bb7c7bb1a470aabc10d44256c4d4559d916", + "salt": "efa8bff96212b2f4a3f371a10d574152655f5dfb", + "signature": "7e0935ea18f4d6c1d17ce82eb2b3836c55b384589ce19dfe743363ac9948d1f346b7bfddfe92efd78adb21faefc89ade42b10f374003fe122e67429a1cb8cbd1f8d9014564c44d120116f4990f1a6e38774c194bd1b8213286b077b0499d2e7b3f434ab12289c556684deed78131934bb3dd6537236f7c6f3dcb09d476be07721e37e1ceed9b2f7b406887bd53157305e1c8b4f84d733bc1e186fe06cc59b6edb8f4bd7ffefdf4f7ba9cfb9d570689b5a1a4109a746a690893db3799255a0cb9215d2d1cd490590e952e8c8786aa0011265252470c041dfbc3eec7c3cbf71c24869d115c0cb4a956f56d530b80ab589acfefc690751ddf36e8d383f83cedd2cc" + }, + { + "message": "25f10895a87716c137450bb9519dfaa1f207faa942ea88abf71e9c17980085b555aebab76264ae2a3ab93c2d12981191ddac6fb5949eb36aee3c5da940f00752c916d94608fa7d97ba6a2915b688f20323d4e9d96801d89a72ab5892dc2117c07434fcf972e058cf8c41ca4b4ff554f7d5068ad3155fced0f3125bc04f9193378a8f5c4c3b8cb4dd6d1cc69d30ecca6eaa51e36a05730e9e342e855baf099defb8afd7", + "salt": "ad8b1523703646224b660b550885917ca2d1df28", + "signature": "6d3b5b87f67ea657af21f75441977d2180f91b2c5f692de82955696a686730d9b9778d970758ccb26071c2209ffbd6125be2e96ea81b67cb9b9308239fda17f7b2b64ecda096b6b935640a5a1cb42a9155b1c9ef7a633a02c59f0d6ee59b852c43b35029e73c940ff0410e8f114eed46bbd0fae165e42be2528a401c3b28fd818ef3232dca9f4d2a0f5166ec59c42396d6c11dbc1215a56fa17169db9575343ef34f9de32a49cdc3174922f229c23e18e45df9353119ec4319cedce7a17c64088c1f6f52be29634100b3919d38f3d1ed94e6891e66a73b8fb849f5874df59459e298c7bbce2eee782a195aa66fe2d0732b25e595f57d3e061b1fc3e4063bf98f" + } + ] + } +} \ No newline at end of file diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index 81b2c109b69b20..241f2b6ee914d4 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -2,6 +2,8 @@ const common = require('../common'); const assert = require('assert'); const fs = require('fs'); +const path = require('path'); +const exec = require('child_process').exec; if (!common.hasCrypto) { common.skip('missing crypto'); @@ -12,6 +14,7 @@ const crypto = require('crypto'); // Test certificates const certPem = fs.readFileSync(common.fixturesDir + '/test_cert.pem', 'ascii'); const keyPem = fs.readFileSync(common.fixturesDir + '/test_key.pem', 'ascii'); +const modSize = 1024; // Test signing and verifying { @@ -71,9 +74,203 @@ const keyPem = fs.readFileSync(common.fixturesDir + '/test_key.pem', 'ascii'); assert.strictEqual(verified, true, 'sign and verify (stream)'); } +// Special tests for RSA_PKCS1_PSS_PADDING +{ + function testPSS(algo, hLen) { + // Maximum permissible salt length + const max = modSize / 8 - hLen - 2; + + function getEffectiveSaltLength(saltLength) { + switch (saltLength) { + case crypto.constants.RSA_PSS_SALTLEN_DIGEST: + return hLen; + case crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN: + return max; + default: + return saltLength; + } + } + + const signSaltLengths = [ + crypto.constants.RSA_PSS_SALTLEN_DIGEST, + getEffectiveSaltLength(crypto.constants.RSA_PSS_SALTLEN_DIGEST), + crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN, + getEffectiveSaltLength(crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN), + 0, 16, 32, 64, 128 + ]; + + const verifySaltLengths = [ + crypto.constants.RSA_PSS_SALTLEN_DIGEST, + getEffectiveSaltLength(crypto.constants.RSA_PSS_SALTLEN_DIGEST), + getEffectiveSaltLength(crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN), + 0, 16, 32, 64, 128 + ]; + + signSaltLengths.forEach((signSaltLength) => { + if (signSaltLength > max) { + // If the salt length is too big, an Error should be thrown + assert.throws(() => { + crypto.createSign(algo) + .update('Test123') + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: signSaltLength + }); + }, /^Error:.*data too large for key size$/); + } else { + // Otherwise, a valid signature should be generated + const s4 = crypto.createSign(algo) + .update('Test123') + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: signSaltLength + }); + + let verified; + verifySaltLengths.forEach((verifySaltLength) => { + // Verification should succeed if and only if the salt length is + // correct + verified = crypto.createVerify(algo) + .update('Test123') + .verify({ + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: verifySaltLength + }, s4); + const saltLengthCorrect = getEffectiveSaltLength(signSaltLength) == + getEffectiveSaltLength(verifySaltLength); + assert.strictEqual(verified, saltLengthCorrect, 'verify (PSS)'); + }); + + // Verification using RSA_PSS_SALTLEN_AUTO should always work + verified = crypto.createVerify(algo) + .update('Test123') + .verify({ + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO + }, s4); + assert.strictEqual(verified, true, 'verify (PSS with SALTLEN_AUTO)'); + + // Verifying an incorrect message should never work + verified = crypto.createVerify(algo) + .update('Test1234') + .verify({ + key: certPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO + }, s4); + assert.strictEqual(verified, false, 'verify (PSS, incorrect)'); + } + }); + } + + testPSS('RSA-SHA1', 20); + testPSS('RSA-SHA256', 32); +} + +// Test vectors for RSA_PKCS1_PSS_PADDING provided by the RSA Laboratories: +// https://www.emc.com/emc-plus/rsa-labs/standards-initiatives/pkcs-rsa-cryptography-standard.htm +{ + // We only test verification as we cannot specify explicit salts when signing + function testVerify(cert, vector) { + const verified = crypto.createVerify('RSA-SHA1') + .update(Buffer.from(vector.message, 'hex')) + .verify({ + key: cert, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: vector.salt.length / 2 + }, vector.signature, 'hex'); + assert.strictEqual(verified, true, 'verify (PSS)'); + } + + const vectorfile = path.join(common.fixturesDir, 'pss-vectors.json'); + const examples = JSON.parse(fs.readFileSync(vectorfile, { + encoding: 'utf8' + })); + + for (const key in examples) { + const example = examples[key]; + const publicKey = example.publicKey.join('\n'); + example.tests.forEach((test) => testVerify(publicKey, test)); + } +} + +// Test exceptions for invalid `padding` and `saltLength` values +{ + [null, undefined, NaN, 'boom', {}, [], true, false] + .forEach((invalidValue) => { + assert.throws(() => { + crypto.createSign('RSA-SHA256') + .update('Test123') + .sign({ + key: keyPem, + padding: invalidValue + }); + }, /^TypeError: padding must be an integer$/); + + assert.throws(() => { + crypto.createSign('RSA-SHA256') + .update('Test123') + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: invalidValue + }); + }, /^TypeError: saltLength must be an integer$/); + }); + + assert.throws(() => { + crypto.createSign('RSA-SHA1') + .update('Test123') + .sign({ + key: keyPem, + padding: crypto.constants.RSA_PKCS1_OAEP_PADDING + }); + }, /^Error:.*illegal or unsupported padding mode$/); +} + // Test throws exception when key options is null { assert.throws(() => { crypto.createSign('RSA-SHA1').update('Test123').sign(null, 'base64'); }, /^Error: No key provided to sign$/); } + +// RSA-PSS Sign test by verifying with 'openssl dgst -verify' +{ + if (!common.opensslCli) { + common.skip('node compiled without OpenSSL CLI.'); + return; + } + + const pubfile = path.join(common.fixturesDir, 'keys/rsa_public_2048.pem'); + const privfile = path.join(common.fixturesDir, 'keys/rsa_private_2048.pem'); + const privkey = fs.readFileSync(privfile); + + const msg = 'Test123'; + const s5 = crypto.createSign('RSA-SHA256') + .update(msg) + .sign({ + key: privkey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING + }); + + common.refreshTmpDir(); + + const sigfile = path.join(common.tmpDir, 's5.sig'); + fs.writeFileSync(sigfile, s5); + const msgfile = path.join(common.tmpDir, 's5.msg'); + fs.writeFileSync(msgfile, msg); + + const cmd = '"' + common.opensslCli + '" dgst -sha256 -verify "' + pubfile + + '" -signature "' + sigfile + + '" -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-2 "' + + msgfile + '"'; + + exec(cmd, common.mustCall((err, stdout, stderr) => { + assert(stdout.includes('Verified OK')); + })); +}