diff options
author | Fedor Indutny <fedor.indutny@gmail.com> | 2012-07-11 23:54:20 +0400 |
---|---|---|
committer | Fedor Indutny <fedor.indutny@gmail.com> | 2012-07-20 00:53:36 +0400 |
commit | 8ba189b8d32e3c6489e142101d3e37dcbc551179 (patch) | |
tree | 45d3e19ccc3a8272170f53b7da622f7485966a4e /lib | |
parent | 2b3ba3f538e9ff22cdb4172799ee38d06818165f (diff) | |
download | nodejs-8ba189b8d32e3c6489e142101d3e37dcbc551179.tar.gz nodejs-8ba189b8d32e3c6489e142101d3e37dcbc551179.tar.bz2 nodejs-8ba189b8d32e3c6489e142101d3e37dcbc551179.zip |
tls: veryify server's identity
Diffstat (limited to 'lib')
-rw-r--r-- | lib/http.js | 14 | ||||
-rw-r--r-- | lib/tls.js | 110 |
2 files changed, 113 insertions, 11 deletions
diff --git a/lib/http.js b/lib/http.js index eccea9944..5e7c0ba16 100644 --- a/lib/http.js +++ b/lib/http.js @@ -1123,13 +1123,11 @@ Agent.prototype.removeSocket = function(s, name, host, port, localAddress) { } if (this.requests[name] && this.requests[name].length) { // If we have pending requests and a socket gets closed a new one - this.createSocket( - name, - host, - port, - localAddress, - this.requests[name][0] - ).emit('free'); + this.createSocket(name, + host, + port, + localAddress, + this.requests[name][0]).emit('free'); } }; @@ -1141,7 +1139,7 @@ function ClientRequest(options, cb) { var self = this; OutgoingMessage.call(self); - this.options = options; + this.options = util._extend({}, options); self.agent = options.agent === undefined ? globalAgent : options.agent; var defaultPort = options.defaultPort || 80; diff --git a/lib/tls.js b/lib/tls.js index 5b8f179ba..be24283f4 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -22,6 +22,7 @@ var crypto = require('crypto'); var util = require('util'); var net = require('net'); +var url = require('url'); var events = require('events'); var Stream = require('stream'); var END_OF_FILE = 42; @@ -78,6 +79,98 @@ function convertNPNProtocols(NPNProtocols, out) { } +function checkServerIdentity(host, cert) { + // Create regexp to much hostnames + function regexpify(host, wildcards) { + // Add trailing dot (make hostnames uniform) + if (!/\.$/.test(host)) host += '.'; + + // Host names with less than one dots are considered too broad, + // and should not be allowed + if (!/^.+\..+$/.test(host)) return /$./; + + // The same applies to hostname with more than one wildcard, + // if hostname has wildcard when wildcards are not allowed, + // or if there are less than two dots after wildcard (i.e. *.com or *d.com) + if (/\*.*\*/.test(host) || !wildcards && /\*/.test(host) || + /\*/.test(host) && !/\*.*\..+\..+/.test(host)) { + return /$./; + } + + // Replace wildcard chars with regexp's wildcard and + // escape all characters that have special meaning in regexps + // (i.e. '.', '[', '{', '*', and others) + var re = host.replace( + /\*([a-z0-9\\-_\.])|[\.,\-\\\^\$+?*\[\]\(\):!\|{}]/g, + function(all, sub) { + if (sub) return '[a-z0-9\\-_]*' + (sub === '-' ? '\\-' : sub); + return '\\' + all; + } + ); + + return new RegExp('^' + re + '$', 'i'); + } + + var dnsNames = [], + uriNames = [], + ips = [], + valid = false; + + // There're several names to perform check against: + // CN and altnames in certificate extension + // (DNS names, IP addresses, and URIs) + // + // Walk through altnames and generate lists of those names + if (cert.subjectaltname) { + cert.subjectaltname.split(/, /g).forEach(function(altname) { + if (/^DNS:/.test(altname)) { + dnsNames.push(altname.slice(4)); + } else if (/^IP Address:/.test(altname)) { + ips.push(altname.slice(11)); + } else if (/^URI:/.test(altname)) { + var uri = url.parse(altname.slice(4)); + if (uri) uriNames.push(uri.hostname); + } + }); + } + + // If hostname is an IP address, it should be present in the list of IP + // addresses. + if (net.isIP(host)) { + valid = ips.some(function(ip) { + return ip === host; + }); + } else { + // Transform hostname to canonical form + if (!/\.$/.test(host)) host += '.'; + + // Otherwise check all DNS/URI records from certificate + // (with allowed wildcards) + dnsNames = dnsNames.map(function(name) { + return regexpify(name, true); + }); + + // Wildcards ain't allowed in URI names + uriNames = uriNames.map(function(name) { + return regexpify(name, false); + }); + + dnsNames = dnsNames.concat(uriNames); + + // And only after check if hostname matches CN + // (because CN is deprecated, but should be used for compatiblity anyway) + dnsNames.push(regexpify(cert.subject.CN, false)); + + valid = dnsNames.some(function(re) { + return re.test(host); + }); + } + + return valid; +}; +exports.checkServerIdentity = checkServerIdentity; + + function SlabBuffer() { this.create(); } @@ -1149,11 +1242,12 @@ exports.connect = function(/* [port, host], options, cb */) { var sslcontext = crypto.createCredentials(options); convertNPNProtocols(options.NPNProtocols, this); - var pair = new SecurePair(sslcontext, false, true, + var hostname = options.servername || options.host, + pair = new SecurePair(sslcontext, false, true, options.rejectUnauthorized === true ? true : false, { NPNProtocols: this.NPNProtocols, - servername: options.servername || options.host + servername: hostname }); if (options.session) { @@ -1178,9 +1272,19 @@ exports.connect = function(/* [port, host], options, cb */) { cleartext.npnProtocol = pair.npnProtocol; + // Verify that server's identity matches it's certificate's names + if (!verifyError) { + var validCert = checkServerIdentity(hostname, + pair.cleartext.getPeerCertificate()); + if (!validCert) { + verifyError = new Error('Hostname/IP doesn\'t match certificate\'s ' + + 'altnames'); + } + } + if (verifyError) { cleartext.authorized = false; - cleartext.authorizationError = verifyError; + cleartext.authorizationError = verifyError.message; if (pair._rejectUnauthorized) { cleartext.emit('error', verifyError); |