From f755ecf484a9789525746475b924ddf2b3f316d0 Mon Sep 17 00:00:00 2001 From: Thom Seddon Date: Fri, 4 Oct 2013 12:59:38 +0100 Subject: [PATCH] src: accept passphrase when crypto signing with private key Previous behaviour was to drop to an openssl prompt ("Enter PEM pass phrase:") when supplying a private key with a passphrase. This change adds a fourth, optional, paramter that will be used as the passphrase. To include this parameter in a backwards compatible way it was necessary to expose the previously undocumented (and unexposed) feature of being able to explitly setting the output encoding. --- doc/api/crypto.markdown | 11 ++- lib/crypto.js | 11 ++- src/node_crypto.cc | 99 +++++++++++++++----- src/node_crypto.h | 9 +- test/fixtures/test_dsa_privkey_encrypted.pem | 23 +++++ test/fixtures/test_rsa_privkey_encrypted.pem | 18 ++++ test/simple/test-crypto.js | 73 +++++++++++++-- 7 files changed, 203 insertions(+), 41 deletions(-) create mode 100644 test/fixtures/test_dsa_privkey_encrypted.pem create mode 100644 test/fixtures/test_rsa_privkey_encrypted.pem diff --git a/doc/api/crypto.markdown b/doc/api/crypto.markdown index 925eb58933b..e2d7bb13a7f 100644 --- a/doc/api/crypto.markdown +++ b/doc/api/crypto.markdown @@ -290,8 +290,15 @@ with new data as it is streamed. ### sign.sign(private_key, [output_format]) Calculates the signature on all the updated data passed through the -sign. `private_key` is a string containing the PEM encoded private -key for signing. +sign. + +`private_key` can be an object or a string. If `private_key` is a string, it is +treated as the key with no passphrase. + +`private_key`: + +* `key` : A string holding the PEM encoded private key +* `passphrase` : A string of passphrase for the private key Returns the signature in `output_format` which can be `'binary'`, `'hex'` or `'base64'`. If no encoding is provided, then a buffer is diff --git a/lib/crypto.js b/lib/crypto.js index 4f4e7e1c80b..1434cd1b1f7 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -382,10 +382,15 @@ Sign.prototype._write = function(chunk, encoding, callback) { Sign.prototype.update = Hash.prototype.update; -Sign.prototype.sign = function(key, encoding) { - encoding = encoding || exports.DEFAULT_ENCODING; - var ret = this._binding.sign(toBuf(key)); +Sign.prototype.sign = function(options, encoding) { + if (!options) + throw new Error('No key provided to sign'); + var key = options.key || options; + var passphrase = options.passphrase || null; + var ret = this._binding.sign(toBuf(key), null, passphrase); + + encoding = encoding || exports.DEFAULT_ENCODING; if (encoding && encoding !== 'buffer') ret = ret.toString(encoding); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index de9b80328cb..5051c84a790 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -164,6 +164,19 @@ static void crypto_lock_cb(int mode, int n, const char* file, int line) { } +static int CryptoPemCallback(char *buf, int size, int rwflag, void *u) { + if (u) { + size_t buflen = static_cast(size); + size_t len = strlen(static_cast(u)); + len = len > buflen ? buflen : len; + memcpy(buf, u, len); + return len; + } + + return 0; +} + + void ThrowCryptoErrorHelper(unsigned long err, bool is_type_error) { HandleScope scope(node_isolate); char errmsg[128]; @@ -342,7 +355,7 @@ static X509* LoadX509(Handle v) { if (!bio) return NULL; - X509 * x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL); + X509 * x509 = PEM_read_bio_X509(bio, NULL, CryptoPemCallback, NULL); if (!x509) { BIO_free_all(bio); return NULL; @@ -372,7 +385,9 @@ void SecureContext::SetKey(const FunctionCallbackInfo& args) { String::Utf8Value passphrase(args[1]); - EVP_PKEY* key = PEM_read_bio_PrivateKey(bio, NULL, NULL, + EVP_PKEY* key = PEM_read_bio_PrivateKey(bio, + NULL, + CryptoPemCallback, len == 1 ? NULL : *passphrase); if (!key) { @@ -399,7 +414,7 @@ int SSL_CTX_use_certificate_chain(SSL_CTX *ctx, BIO *in) { int ret = 0; X509 *x = NULL; - x = PEM_read_bio_X509_AUX(in, NULL, NULL, NULL); + x = PEM_read_bio_X509_AUX(in, NULL, CryptoPemCallback, NULL); if (x == NULL) { SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_CHAIN_FILE, ERR_R_PEM_LIB); @@ -425,7 +440,7 @@ int SSL_CTX_use_certificate_chain(SSL_CTX *ctx, BIO *in) { ctx->extra_certs = NULL; } - while ((ca = PEM_read_bio_X509(in, NULL, NULL, NULL))) { + while ((ca = PEM_read_bio_X509(in, NULL, CryptoPemCallback, NULL))) { r = SSL_CTX_add_extra_chain_cert(ctx, ca); if (!r) { @@ -530,7 +545,7 @@ void SecureContext::AddCRL(const FunctionCallbackInfo& args) { if (!bio) return; - X509_CRL *x509 = PEM_read_bio_X509_CRL(bio, NULL, NULL, NULL); + X509_CRL *x509 = PEM_read_bio_X509_CRL(bio, NULL, CryptoPemCallback, NULL); if (x509 == NULL) { BIO_free_all(bio); @@ -564,7 +579,7 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo& args) { return; } - X509 *x509 = PEM_read_bio_X509(bp, NULL, NULL, NULL); + X509 *x509 = PEM_read_bio_X509(bp, NULL, CryptoPemCallback, NULL); if (x509 == NULL) { BIO_free_all(bp); @@ -2634,28 +2649,57 @@ void Sign::SignUpdate(const FunctionCallbackInfo& args) { } -bool Sign::SignFinal(unsigned char** md_value, - unsigned int *md_len, - const char* key_pem, - int key_pem_len) { - if (!initialised_) +bool Sign::SignFinal(const char* key_pem, + int key_pem_len, + const char* passphrase, + unsigned char** sig, + unsigned int *sig_len) { + if (!initialised_) { + ThrowError("Sign not initalised"); return false; + } BIO* bp = NULL; EVP_PKEY* pkey = NULL; + bool fatal = true; + bp = BIO_new(BIO_s_mem()); + if (bp == NULL) + goto exit; + if (!BIO_write(bp, key_pem, key_pem_len)) - return false; + goto exit; - pkey = PEM_read_bio_PrivateKey(bp, NULL, NULL, NULL); + pkey = PEM_read_bio_PrivateKey(bp, + NULL, + CryptoPemCallback, + const_cast(passphrase)); if (pkey == NULL) - return 0; + goto exit; + + if (EVP_SignFinal(&mdctx_, *sig, sig_len, pkey)) + fatal = false; - EVP_SignFinal(&mdctx_, *md_value, md_len, pkey); - EVP_MD_CTX_cleanup(&mdctx_); initialised_ = false; - EVP_PKEY_free(pkey); - BIO_free_all(bp); + + exit: + if (pkey != NULL) + EVP_PKEY_free(pkey); + if (bp != NULL) + BIO_free_all(bp); + + EVP_MD_CTX_cleanup(&mdctx_); + + if (fatal) { + unsigned long err = ERR_get_error(); + if (err) { + ThrowCryptoError(err); + } else { + ThrowError("PEM_read_bio_PrivateKey"); + } + return false; + } + return true; } @@ -2668,19 +2712,26 @@ void Sign::SignFinal(const FunctionCallbackInfo& args) { unsigned char* md_value; unsigned int md_len; + unsigned int len = args.Length(); enum encoding encoding = BUFFER; - if (args.Length() >= 2) { + if (len >= 2 && args[1]->IsString()) { encoding = ParseEncoding(args[1]->ToString(), BUFFER); } + String::Utf8Value passphrase(args[2]); + ASSERT_IS_BUFFER(args[0]); - ssize_t len = Buffer::Length(args[0]); + size_t buf_len = Buffer::Length(args[0]); char* buf = Buffer::Data(args[0]); md_len = 8192; // Maximum key size is 8192 bits md_value = new unsigned char[md_len]; - bool r = sign->SignFinal(&md_value, &md_len, buf, len); + bool r = sign->SignFinal(buf, + buf_len, + len >= 3 && !args[2]->IsNull() ? *passphrase : NULL, + &md_value, + &md_len); if (!r) { delete[] md_value; md_value = NULL; @@ -2811,11 +2862,11 @@ bool Verify::VerifyFinal(const char* key_pem, // Split this out into a separate function once we have more than one // consumer of public keys. if (strncmp(key_pem, PUBLIC_KEY_PFX, PUBLIC_KEY_PFX_LEN) == 0) { - pkey = PEM_read_bio_PUBKEY(bp, NULL, NULL, NULL); + pkey = PEM_read_bio_PUBKEY(bp, NULL, CryptoPemCallback, NULL); if (pkey == NULL) goto exit; } else if (strncmp(key_pem, PUBRSA_KEY_PFX, PUBRSA_KEY_PFX_LEN) == 0) { - RSA* rsa = PEM_read_bio_RSAPublicKey(bp, NULL, NULL, NULL); + RSA* rsa = PEM_read_bio_RSAPublicKey(bp, NULL, CryptoPemCallback, NULL); if (rsa) { pkey = EVP_PKEY_new(); if (pkey) @@ -2826,7 +2877,7 @@ bool Verify::VerifyFinal(const char* key_pem, goto exit; } else { // X.509 fallback - x509 = PEM_read_bio_X509(bp, NULL, NULL, NULL); + x509 = PEM_read_bio_X509(bp, NULL, CryptoPemCallback, NULL); if (x509 == NULL) goto exit; diff --git a/src/node_crypto.h b/src/node_crypto.h index 094105eed29..f6e3276a54a 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -434,10 +434,11 @@ class Sign : public WeakObject { void SignInit(const char* sign_type); bool SignUpdate(const char* data, int len); - bool SignFinal(unsigned char** md_value, - unsigned int *md_len, - const char* key_pem, - int key_pem_len); + bool SignFinal(const char* key_pem, + int key_pem_len, + const char* passphrase, + unsigned char** sig, + unsigned int *sig_len); protected: static void New(const v8::FunctionCallbackInfo& args); diff --git a/test/fixtures/test_dsa_privkey_encrypted.pem b/test/fixtures/test_dsa_privkey_encrypted.pem new file mode 100644 index 00000000000..0eb65bb451f --- /dev/null +++ b/test/fixtures/test_dsa_privkey_encrypted.pem @@ -0,0 +1,23 @@ +-----BEGIN DSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,2E8DE7F5BD338C4118488E8D640FC695 + +7jnL7kBITpnfjHc4DiRF+9d0M9QKdQmiL9N7Bj52XC+L0jZTwfNld3xi6fQ1GNle +RKrrgSgEYXxf+RJ9Nz/BOttYWnIyAWSswFIjm1vGjpoYTH6H/wFg1QSoZUfINO2I +3p4Y+cYVWOgSAYegzT5sdWTKDJrRUUfYFThmdQk0uO3s8B7urQuVlEtHr02OuPAj +hRiWaBtxkttBWA+x8dgpgAbjHlZIWv1fj0EAKaaVehaNzEyK+nyS1a816ssDV3t8 +YMOZdCdKgzbBr5T3Zf83hdzpmhagBNZve1P0kJEgGdydiRWSyTxotOt5AGsRSvza +A9PVk8V/U6U1B18hACxGV4wiKCMQDAsHfo+BrVoZBvVBlpW4dfcsIEtQqwu18x2b +wIW5Qc2zXFFL6P+eqfdZ0ZRdfsClX6/GYOO5Z4oy8iAQSuD1UdaG6Psy84U7LU8g +++OcbEcw8UnKjKVJU+zBC4QmhxUSUiOQwcgeFQuMIEMtprUKztgm/oPClAMTY/pm +FGxLZS59owVWkrN9Oc4ccw+6Zt6mDxH9cnHv+nkGlcK9pcD+gU1MVXUfuby+DNbI +4iYqUoYZdb9gpWQ/VrXMX63NydXzE+vMB9BxOlgfw3b6BrFCUAuyH1FiIAlGeAjP +LZa06WiOayeu6Lm8rzeu/Cjbe1pYzK9cyX3JxSGJxipPeO4URZ5+hyqBMyCCCUq8 +EVFcfwgkdQaeVeBUdxJyRXfuBQmlgJF0Ixlkw29StpI2dAbNrtcSAIwbsxDInK4b +6ItdadW+0nCRAxdVbGt6oQIPqpjbmtVkqj+m1yAic1xYc7Kd2xngGdtOMefKefcw ++7d7E82ljPycHDG2SNENsFV9TNENdNlaP1A1HQy+f/1YkLZHfNLQrUf1+BRR7oHI +N0ACLF6jgZ9MFelB64774veUTLvcrmYKIX7TnV25kw28ZIQ8StmIt9YJ+Mq+x6DC +32JbRBbwbHm598fCrfr471xw/SM1/OnPefVhJSQ6223IfjuSWG0Snvjo7mHbaduz +xWW6ApT7/iilanZs8uKBuPaEtwu3CmJcdgj0tTUuXb5ivY3M0dD/ZLSQliqb49iU +64LX0/kRvkUZ6nJqPA4nlKPfGebo6H0V4oX7XF/gm74= +-----END DSA PRIVATE KEY----- diff --git a/test/fixtures/test_rsa_privkey_encrypted.pem b/test/fixtures/test_rsa_privkey_encrypted.pem new file mode 100644 index 00000000000..08e76171942 --- /dev/null +++ b/test/fixtures/test_rsa_privkey_encrypted.pem @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,9D916E00476DFF9E70FA4BA9E3A6CB0E + +oj0VC35ShSEqlfJ0rLGgkqJCyIK+mXSsa/X/xAur+lI/RVOVTWd7oQQGTdI/0rLX +PdQR02Na3X9Rptezh6J04PfMGeFysxdT6RpC+rkHRPVbN0F4TqxSNNXzkwK70+EF +dSuDMyVKv9YN4wWDf0g6VKe4ShAH/sqICQBrVyzWyYLvH/hwZmZZ1QEab6ylIKtb +EJunwu9BxVVA04bbuATKkKjJOqDn0fG8hb4bYbyD02dJwgLePzzn36F31kcBCEHI +tESlD3RsS+EtfpfgPkplXNOhqYzkD9auDb7Zy+ZwL20fjnJb75OSGu8gOg3KTljt +mApZOg0nJ5Jk9ATAdyzyVSFOM1Hhcw12ws06Dq9KRnXgO6bbuadLTFRDdvSYDFvD +ijUb+97UolQfYIXQMqXli3EIvHr7CTWe/3mpoDgK1mtr0+923Bm97XgE7KSr0L46 +n5QpNjCZf1vbXldNmW+TRifiJMgtVdS7x0N4vqDPNEe+FelVv3U4Pz3HIOtFuWLr +ZCxlgVxJY4IsyYlV0ItQjIv8fJiAyemZdO2lA9K6h0eEF+9Apr3i79JGWUi74p5D +Ooak4le0Va9O34f6FxCGn/a54A6bhKu24Ub/0gr/e4WRa7693euEdgIAZXhtMu2Z +taU5SKjjXPzjmRCM2kINHTCENlaU4oFzTmj3TYY/jdKyNP1bHa07NhlomladkIHK +GD6HaYkcbuwvh8hOPsopSwuS+NqjnGPq9Vv4ecBC+9veDEmpIE1iR6FK9Hjrre88 +kLoMQNmA+vuc8jG4/FIHM3SauQiR1ZJ6+zkz97kcmOf+X7LRaS4j6lfFR6qHiJ6y +-----END RSA PRIVATE KEY----- diff --git a/test/simple/test-crypto.js b/test/simple/test-crypto.js index 95fdf7fc414..22ce55f80d5 100644 --- a/test/simple/test-crypto.js +++ b/test/simple/test-crypto.js @@ -46,6 +46,15 @@ var rsaPubPem = fs.readFileSync(common.fixturesDir + '/test_rsa_pubkey.pem', 'ascii'); var rsaKeyPem = fs.readFileSync(common.fixturesDir + '/test_rsa_privkey.pem', 'ascii'); +var rsaKeyPemEncrypted = fs.readFileSync( + common.fixturesDir + '/test_rsa_privkey_encrypted.pem', 'ascii'); +var dsaPubPem = fs.readFileSync(common.fixturesDir + '/test_dsa_pubkey.pem', + 'ascii'); +var dsaKeyPem = fs.readFileSync(common.fixturesDir + '/test_dsa_privkey.pem', + 'ascii'); +var dsaKeyPemEncrypted = fs.readFileSync( + common.fixturesDir + '/test_dsa_privkey_encrypted.pem', 'ascii'); + try { var credentials = crypto.createCredentials( @@ -761,6 +770,30 @@ assert.equal(rsaSignature, rsaVerify.update(rsaPubPem); assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); +// Test RSA key signing/verification with encrypted key +rsaSign = crypto.createSign('RSA-SHA1'); +rsaSign.update(rsaPubPem); +assert.doesNotThrow(function() { + var signOptions = { key: rsaKeyPemEncrypted, passphrase: 'password' }; + rsaSignature = rsaSign.sign(signOptions, 'hex'); +}); +assert.equal(rsaSignature, + '5c50e3145c4e2497aadb0eabc83b342d0b0021ece0d4c4a064b7c' + + '8f020d7e2688b122bfb54c724ac9ee169f83f66d2fe90abeb95e8' + + 'e1290e7e177152a4de3d944cf7d4883114a20ed0f78e70e25ef0f' + + '60f06b858e6af42a2f276ede95bbc6bc9a9bbdda15bd663186a6f' + + '40819a7af19e577bb2efa5e579a1f5ce8a0d4ca8b8f6'); + +rsaVerify = crypto.createVerify('RSA-SHA1'); +rsaVerify.update(rsaPubPem); +assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); + +rsaSign = crypto.createSign('RSA-SHA1'); +rsaSign.update(rsaPubPem); +assert.throws(function() { + var signOptions = { key: rsaKeyPemEncrypted, passphrase: 'wrong' }; + rsaSign.sign(signOptions, 'hex'); +}); // // Test RSA signing and verification @@ -798,24 +831,48 @@ assert.strictEqual(rsaVerify.verify(rsaPubPem, rsaSignature, 'hex'), true); // Test DSA signing and verification // (function() { - var privateKey = fs.readFileSync( - common.fixturesDir + '/test_dsa_privkey.pem'); - - var publicKey = fs.readFileSync( - common.fixturesDir + '/test_dsa_pubkey.pem'); - var input = 'I AM THE WALRUS'; // DSA signatures vary across runs so there is no static string to verify // against var sign = crypto.createSign('DSS1'); sign.update(input); - var signature = sign.sign(privateKey, 'hex'); + var signature = sign.sign(dsaKeyPem, 'hex'); var verify = crypto.createVerify('DSS1'); verify.update(input); - assert.strictEqual(verify.verify(publicKey, signature, 'hex'), true); + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); +})(); + + +// +// Test DSA signing and verification with encrypted key +// +(function() { + var input = 'I AM THE WALRUS'; + + var sign = crypto.createSign('DSS1'); + sign.update(input); + assert.throws(function() { + sign.sign({ key: dsaKeyPemEncrypted, passphrase: 'wrong' }, 'hex'); + }); + + // DSA signatures vary across runs so there is no static string to verify + // against + var sign = crypto.createSign('DSS1'); + sign.update(input); + + var signature; + assert.doesNotThrow(function() { + var signOptions = { key: dsaKeyPemEncrypted, passphrase: 'password' }; + signature = sign.sign(signOptions, 'hex'); + }); + + var verify = crypto.createVerify('DSS1'); + verify.update(input); + + assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); })();