src: accept passphrase when crypto signing with private key
authorThom Seddon <thom@nightworld.com>
Fri, 4 Oct 2013 11:59:38 +0000 (12:59 +0100)
committerFedor Indutny <fedor.indutny@gmail.com>
Tue, 29 Oct 2013 10:19:47 +0000 (14:19 +0400)
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
lib/crypto.js
src/node_crypto.cc
src/node_crypto.h
test/fixtures/test_dsa_privkey_encrypted.pem [new file with mode: 0644]
test/fixtures/test_rsa_privkey_encrypted.pem [new file with mode: 0644]
test/simple/test-crypto.js

index 925eb58..e2d7bb1 100644 (file)
@@ -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
index 4f4e7e1..1434cd1 100644 (file)
@@ -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);
 
index de9b803..5051c84 100644 (file)
@@ -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_t>(size);
+    size_t len = strlen(static_cast<const char*>(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<Value> 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<Value>& 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<Value>& 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<Value>& 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<Value>& 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<char*>(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<Value>& 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;
 
index 094105e..f6e3276 100644 (file)
@@ -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<v8::Value>& args);
diff --git a/test/fixtures/test_dsa_privkey_encrypted.pem b/test/fixtures/test_dsa_privkey_encrypted.pem
new file mode 100644 (file)
index 0000000..0eb65bb
--- /dev/null
@@ -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 (file)
index 0000000..08e7617
--- /dev/null
@@ -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-----
index 95fdf7f..22ce55f 100644 (file)
@@ -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 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(dsaKeyPem, 'hex');
+
+  var verify = crypto.createVerify('DSS1');
+  verify.update(input);
+
+  assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true);
+})();
 
-  var publicKey = fs.readFileSync(
-      common.fixturesDir + '/test_dsa_pubkey.pem');
 
+//
+// 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 = sign.sign(privateKey, 'hex');
+
+  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(publicKey, signature, 'hex'), true);
+  assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true);
 })();