From f8c335d0caf47f16d31413f89aa28eda3878e3aa Mon Sep 17 00:00:00 2001 From: koichik Date: Wed, 7 Dec 2011 22:47:06 +0900 Subject: [PATCH] tls: enable rejectUnauthorized option to client Fiexes #2247. --- doc/api/https.markdown | 8 ++- doc/api/tls.markdown | 4 ++ lib/tls.js | 13 +++- test/simple/test-https-client-reject.js | 95 +++++++++++++++++++++++++ test/simple/test-tls-client-reject.js | 92 ++++++++++++++++++++++++ 5 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 test/simple/test-https-client-reject.js create mode 100644 test/simple/test-tls-client-reject.js diff --git a/doc/api/https.markdown b/doc/api/https.markdown index 0c019349b4..5de5b4f41b 100644 --- a/doc/api/https.markdown +++ b/doc/api/https.markdown @@ -11,8 +11,8 @@ This class is a subclass of `tls.Server` and emits events same as ## https.createServer(options, [requestListener]) Returns a new HTTPS web server object. The `options` is similar to -`tls.createServer()`. The `requestListener` is a function which is -automatically added to the `'request'` event. +[tls.createServer()](tls.html#tls.createServer). The `requestListener` is +a function which is automatically added to the `'request'` event. Example: @@ -94,6 +94,10 @@ specified. However, a [globalAgent](#https.globalAgent) silently ignores these. - `cert`: Public x509 certificate to use. Default `null`. - `ca`: An authority certificate or array of authority certificates to check the remote host against. +- `rejectUnauthorized`: If `true`, the server certificate is verified against + the list of supplied CAs. An `'error'` event is emitted if verification + fails. Verification happens at the connection level, *before* the HTTP + request is sent. Default `false`. In order to specify these options, use a custom `Agent`. diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index ca60c658c1..d7307d4eee 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -119,6 +119,10 @@ defaults to `localhost`.) `options` should be an object which specifies omitted several well known "root" CAs will be used, like VeriSign. These are used to authorize connections. + - `rejectUnauthorized`: If `true`, the server certificate is verified against + the list of supplied CAs. An `'error'` event is emitted if verification + fails. Default: `false`. + - `NPNProtocols`: An array of string or `Buffer` containing supported NPN protocols. `Buffer` should have following format: `0x05hello0x05world`, where first byte is next protocol name's length. (Passing array should diff --git a/lib/tls.js b/lib/tls.js index 34fe11f058..e4f332d415 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -1023,7 +1023,8 @@ exports.connect = function(port /* host, options, cb */) { var sslcontext = crypto.createCredentials(options); convertNPNProtocols(options.NPNProtocols, this); - var pair = new SecurePair(sslcontext, false, true, false, + var pair = new SecurePair(sslcontext, false, true, + options.rejectUnauthorized === true ? true : false, { NPNProtocols: this.NPNProtocols, servername: options.servername || host @@ -1048,11 +1049,17 @@ exports.connect = function(port /* host, options, cb */) { if (verifyError) { cleartext.authorized = false; cleartext.authorizationError = verifyError; + + if (pair._rejectUnauthorized) { + cleartext.emit('error', verifyError); + pair.destroy(); + } else { + cleartext.emit('secureConnect'); + } } else { cleartext.authorized = true; + cleartext.emit('secureConnect'); } - - cleartext.emit('secureConnect'); }); cleartext._controlReleased = true; diff --git a/test/simple/test-https-client-reject.js b/test/simple/test-https-client-reject.js new file mode 100644 index 0000000000..77820533da --- /dev/null +++ b/test/simple/test-https-client-reject.js @@ -0,0 +1,95 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +if (!process.versions.openssl) { + console.error('Skipping because node compiled without OpenSSL.'); + process.exit(0); +} + +var common = require('../common'); +var assert = require('assert'); +var https = require('https'); +var fs = require('fs'); +var path = require('path'); + +var options = { + key: fs.readFileSync(path.join(common.fixturesDir, 'test_key.pem')), + cert: fs.readFileSync(path.join(common.fixturesDir, 'test_cert.pem')) +}; + +var reqCount = 0; + +var server = https.createServer(options, function(req, res) { + ++reqCount; + res.writeHead(200); + res.end(); +}).listen(common.PORT, function() { + unauthorized(); +}); + +function unauthorized() { + var req = https.request({ + port: common.PORT + }, function(res) { + assert(!req.socket.authorized); + rejectUnauthorized(); + }); + req.on('error', function(err) { + assert(false); + }); + req.end(); +} + +function rejectUnauthorized() { + var options = { + port: common.PORT, + rejectUnauthorized: true + }; + options.agent = new https.Agent(options); + var req = https.request(options, function(res) { + assert(false); + }); + req.on('error', function(err) { + authorized(); + }); + req.end(); +} + +function authorized() { + var options = { + port: common.PORT, + rejectUnauthorized: true, + ca: [ fs.readFileSync(path.join(common.fixturesDir, 'test_cert.pem')) ] + }; + options.agent = new https.Agent(options); + var req = https.request(options, function(res) { + assert(req.socket.authorized); + server.close(); + }); + req.on('error', function(err) { + assert(false); + }); + req.end(); +} + +process.on('exit', function() { + assert.equal(reqCount, 2); +}); diff --git a/test/simple/test-tls-client-reject.js b/test/simple/test-tls-client-reject.js new file mode 100644 index 0000000000..e11d790e60 --- /dev/null +++ b/test/simple/test-tls-client-reject.js @@ -0,0 +1,92 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +if (!process.versions.openssl) { + console.error('Skipping because node compiled without OpenSSL.'); + process.exit(0); +} + +var common = require('../common'); +var assert = require('assert'); +var tls = require('tls'); +var fs = require('fs'); +var path = require('path'); + +var options = { + key: fs.readFileSync(path.join(common.fixturesDir, 'test_key.pem')), + cert: fs.readFileSync(path.join(common.fixturesDir, 'test_cert.pem')) +}; + +var connectCount = 0; + +var server = tls.createServer(options, function(socket) { + ++connectCount; + socket.on('data', function(data) { + common.debug(data.toString()); + assert.equal(data, 'ok'); + }); +}).listen(common.PORT, function() { + unauthorized(); +}); + +function unauthorized() { + var socket = tls.connect(common.PORT, function() { + assert(!socket.authorized); + socket.end(); + rejectUnauthorized(); + }); + socket.on('error', function(err) { + assert(false); + }); + socket.write('ok'); +} + +function rejectUnauthorized() { + var socket = tls.connect(common.PORT, { + rejectUnauthorized: true + }, function() { + assert(false); + }); + socket.on('error', function(err) { + common.debug(err); + authorized(); + }); + socket.write('ng'); +} + +function authorized() { + var socket = tls.connect(common.PORT, { + rejectUnauthorized: true, + ca: [ fs.readFileSync(path.join(common.fixturesDir, 'test_cert.pem')) ] + }, function() { + assert(socket.authorized); + socket.end(); + server.close(); + }); + socket.on('error', function(err) { + assert(false); + }); + socket.write('ok'); +} + +process.on('exit', function() { + assert.equal(connectCount, 3); +});