@@ -96,115 +96,163 @@ function convertNPNProtocols(NPNProtocols, out) {
9696 }
9797}
9898
99+ function unfqdn ( host ) {
100+ return host . replace ( / [ . ] $ / , '' ) ;
101+ }
99102
100- function checkServerIdentity ( host , cert ) {
101- // Create regexp to much hostnames
102- function regexpify ( host , wildcards ) {
103- // Add trailing dot (make hostnames uniform)
104- if ( ! / \. $ / . test ( host ) ) host += '.' ;
105-
106- // The same applies to hostname with more than one wildcard,
107- // if hostname has wildcard when wildcards are not allowed,
108- // or if there are less than two dots after wildcard (i.e. *.com or *d.com)
109- //
110- // also
111- //
112- // "The client SHOULD NOT attempt to match a presented identifier in
113- // which the wildcard character comprises a label other than the
114- // left-most label (e.g., do not match bar.*.example.net)."
115- // RFC6125
116- if ( ! wildcards && / \* / . test ( host ) || / [ \. \* ] .* \* / . test ( host ) ||
117- / \* / . test ( host ) && ! / \* .* \. .+ \. .+ / . test ( host ) ) {
118- return / $ ./ ;
119- }
103+ function splitHost ( host ) {
104+ // String#toLowerCase() is locale-sensitive so we use
105+ // a conservative version that only lowercases A-Z.
106+ function replacer ( c ) {
107+ return String . fromCharCode ( 32 + c . charCodeAt ( 0 ) ) ;
108+ } ;
109+ return unfqdn ( host ) . replace ( / [ A - Z ] / g, replacer ) . split ( '.' ) ;
110+ }
120111
121- // Replace wildcard chars with regexp's wildcard and
122- // escape all characters that have special meaning in regexps
123- // (i.e. '.', '[', '{', '*', and others)
124- var re = host . replace (
125- / \* ( [ a - z 0 - 9 \\ -_ \. ] ) | [ \. , \- \\ \^ \$ + ? * \[ \] \( \) : ! \| { } ] / g,
126- function ( all , sub ) {
127- if ( sub ) return '[a-z0-9\\-_]*' + ( sub === '-' ? '\\-' : sub ) ;
128- return '\\' + all ;
129- } ) ;
130-
131- return new RegExp ( '^' + re + '$' , 'i' ) ;
112+ function check ( hostParts , pattern , wildcards ) {
113+ // Empty strings, null, undefined, etc. never match.
114+ if ( ! pattern )
115+ return false ;
116+
117+ var patternParts = splitHost ( pattern ) ;
118+
119+ if ( hostParts . length !== patternParts . length )
120+ return false ;
121+
122+ // Pattern has empty components, e.g. "bad..example.com".
123+ if ( patternParts . indexOf ( '' ) !== - 1 )
124+ return false ;
125+
126+ // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no
127+ // good way to detect their encoding or normalize them so we simply
128+ // reject them. Control characters and blanks are rejected as well
129+ // because nothing good can come from accepting them.
130+ function isBad ( s ) {
131+ return / [ ^ \u0021 - \u007F ] / . test ( s ) ;
132132 }
133133
134- var dnsNames = [ ] ,
135- uriNames = [ ] ,
136- ips = [ ] ,
137- matchCN = true ,
138- valid = false ;
134+ if ( patternParts . some ( isBad ) )
135+ return false ;
139136
140- // There're several names to perform check against:
141- // CN and altnames in certificate extension
142- // (DNS names, IP addresses, and URIs)
143- //
144- // Walk through altnames and generate lists of those names
145- if ( cert . subjectaltname ) {
146- cert . subjectaltname . split ( / , / g) . forEach ( function ( altname ) {
147- if ( / ^ D N S : / . test ( altname ) ) {
148- dnsNames . push ( altname . slice ( 4 ) ) ;
149- } else if ( / ^ I P A d d r e s s : / . test ( altname ) ) {
150- ips . push ( altname . slice ( 11 ) ) ;
151- } else if ( / ^ U R I : / . test ( altname ) ) {
152- var uri = url . parse ( altname . slice ( 4 ) ) ;
153- if ( uri ) uriNames . push ( uri . hostname ) ;
137+ // Check host parts from right to left first.
138+ for ( var i = hostParts . length - 1 ; i > 0 ; i -= 1 )
139+ if ( hostParts [ i ] !== patternParts [ i ] )
140+ return false ;
141+
142+ var hostSubdomain = hostParts [ 0 ] ;
143+ var patternSubdomain = patternParts [ 0 ] ;
144+ var patternSubdomainParts = patternSubdomain . split ( '*' ) ;
145+
146+ // Short-circuit when the subdomain does not contain a wildcard.
147+ // RFC 6125 does not allow wildcard substitution for components
148+ // containing IDNA A-labels (Punycode) so match those verbatim.
149+ if ( patternSubdomainParts . length === 1 ||
150+ patternSubdomain . indexOf ( 'xn--' ) !== - 1 ) {
151+ return hostSubdomain === patternSubdomain ;
152+ }
153+
154+ if ( ! wildcards )
155+ return false ;
156+
157+ // More than one wildcard is always wrong.
158+ if ( patternSubdomainParts . length > 2 )
159+ return false ;
160+
161+ // *.tld wildcards are not allowed.
162+ if ( patternParts . length <= 2 )
163+ return false ;
164+
165+ var prefix = patternSubdomainParts [ 0 ] ;
166+ var suffix = patternSubdomainParts [ 1 ] ;
167+
168+ if ( prefix . length + suffix . length > hostSubdomain . length )
169+ return false ;
170+
171+ if ( prefix . length > 0 && hostSubdomain . slice ( 0 , prefix . length ) !== prefix )
172+ return false ;
173+
174+ if ( suffix . length > 0 && hostSubdomain . slice ( - suffix . length ) !== suffix )
175+ return false ;
176+
177+ return true ;
178+ }
179+
180+ function _checkServerIdentity ( host , cert ) {
181+ var subject = cert . subject ;
182+ var altNames = cert . subjectaltname ;
183+ var dnsNames = [ ] ;
184+ var uriNames = [ ] ;
185+ var ips = [ ] ;
186+
187+ host = '' + host ;
188+
189+ if ( altNames ) {
190+ altNames . split ( ', ' ) . forEach ( function ( name ) {
191+ if ( / ^ D N S : / . test ( name ) ) {
192+ dnsNames . push ( name . slice ( 4 ) ) ;
193+ } else if ( / ^ U R I : / . test ( name ) ) {
194+ var uri = url . parse ( name . slice ( 4 ) ) ;
195+ uriNames . push ( uri . hostname ) ; // TODO(bnoordhuis) Also use scheme.
196+ } else if ( / ^ I P A d d r e s s : / . test ( name ) ) {
197+ ips . push ( name . slice ( 11 ) ) ;
154198 }
155199 } ) ;
156200 }
157201
158- // If hostname is an IP address, it should be present in the list of IP
159- // addresses.
202+ var valid = false ;
203+ var reason = 'Unknown reason' ;
204+
160205 if ( net . isIP ( host ) ) {
161- valid = ips . some ( function ( ip ) {
162- return ip === host ;
163- } ) ;
164- } else {
165- // Transform hostname to canonical form
166- if ( ! / \. $ / . test ( host ) ) host += '.' ;
206+ valid = ips . indexOf ( host ) !== - 1 ;
207+ if ( ! valid )
208+ reason = 'IP: ' + host + ' is not in the cert\'s list: ' + ips . join ( ', ' ) ;
209+ // TODO(bnoordhuis) Also check URI SANs that are IP addresses.
210+ } else if ( subject ) {
211+ host = unfqdn ( host ) ; // Remove trailing dot for error messages.
212+ var hostParts = splitHost ( host ) ;
213+
214+ function wildcard ( pattern ) {
215+ return check ( hostParts , pattern , true ) ;
216+ }
167217
168- // Otherwise check all DNS/URI records from certificate
169- // (with allowed wildcards)
170- dnsNames = dnsNames . map ( function ( name ) {
171- return regexpify ( name , true ) ;
172- } ) ;
218+ function noWildcard ( pattern ) {
219+ return check ( hostParts , pattern , false ) ;
220+ }
173221
174- // Wildcards ain't allowed in URI names
175- uriNames = uriNames . map ( function ( name ) {
176- return regexpify ( name , false ) ;
177- } ) ;
222+ // Match against Common Name only if no supported identifiers are present.
223+ if ( dnsNames . length === 0 && ips . length === 0 && uriNames . length === 0 ) {
224+ var cn = subject . CN ;
178225
179- dnsNames = dnsNames . concat ( uriNames ) ;
180-
181- if ( dnsNames . length > 0 ) matchCN = false ;
182-
183- // Match against Common Name (CN) only if no supported identifiers are
184- // present.
185- //
186- // "As noted, a client MUST NOT seek a match for a reference identifier
187- // of CN-ID if the presented identifiers include a DNS-ID, SRV-ID,
188- // URI-ID, or any application-specific identifier types supported by the
189- // client."
190- // RFC6125
191- if ( matchCN ) {
192- var commonNames = cert . subject . CN ;
193- if ( Array . isArray ( commonNames ) ) {
194- for ( var i = 0 , k = commonNames . length ; i < k ; ++ i ) {
195- dnsNames . push ( regexpify ( commonNames [ i ] , true ) ) ;
196- }
197- } else {
198- dnsNames . push ( regexpify ( commonNames , true ) ) ;
226+ if ( Array . isArray ( cn ) )
227+ valid = cn . some ( wildcard ) ;
228+ else if ( cn )
229+ valid = wildcard ( cn ) ;
230+
231+ if ( ! valid )
232+ reason = 'Host: ' + host + '. is not cert\'s CN: ' + cn ;
233+ } else {
234+ valid = dnsNames . some ( wildcard ) || uriNames . some ( noWildcard ) ;
235+ if ( ! valid ) {
236+ reason =
237+ 'Host: ' + host + '. is not in the cert\'s altnames: ' + altNames ;
199238 }
200239 }
240+ } else {
241+ reason = 'Cert is empty' ;
242+ }
201243
202- valid = dnsNames . some ( function ( re ) {
203- return re . test ( host ) ;
204- } ) ;
244+ if ( ! valid ) {
245+ var err = new Error ( 'Hostname/IP doesn\'t match certificate\'s altnames' ) ;
246+ err . reason = reason ;
247+ err . host = host ;
248+ err . cert = cert ;
249+ return err ;
205250 }
251+ }
252+ exports . _checkServerIdentity = _checkServerIdentity ;
206253
207- return valid ;
254+ function checkServerIdentity ( host , cert ) {
255+ return ! ! _checkServerIdentity ( host , cert ) ;
208256}
209257exports . checkServerIdentity = checkServerIdentity ;
210258
@@ -1384,12 +1432,8 @@ exports.connect = function(/* [port, host], options, cb */) {
13841432
13851433 // Verify that server's identity matches it's certificate's names
13861434 if ( ! verifyError ) {
1387- var validCert = checkServerIdentity ( hostname ,
1388- pair . cleartext . getPeerCertificate ( ) ) ;
1389- if ( ! validCert ) {
1390- verifyError = new Error ( 'Hostname/IP doesn\'t match certificate\'s ' +
1391- 'altnames' ) ;
1392- }
1435+ verifyError = _checkServerIdentity ( hostname ,
1436+ pair . cleartext . getPeerCertificate ( ) ) ;
13931437 }
13941438
13951439 if ( verifyError ) {
0 commit comments