@@ -56,134 +56,141 @@ exports.convertNPNProtocols = function convertNPNProtocols(NPNProtocols, out) {
5656 }
5757} ;
5858
59- exports . checkServerIdentity = function checkServerIdentity ( host , cert ) {
60- // Create regexp to much hostnames
61- function regexpify ( host , wildcards ) {
62- // Add trailing dot (make hostnames uniform)
63- if ( ! host || ! host . endsWith ( '.' ) ) host += '.' ;
64-
65- // The same applies to hostname with more than one wildcard,
66- // if hostname has wildcard when wildcards are not allowed,
67- // or if there are less than two dots after wildcard (i.e. *.com or *d.com)
68- //
69- // also
70- //
71- // "The client SHOULD NOT attempt to match a presented identifier in
72- // which the wildcard character comprises a label other than the
73- // left-most label (e.g., do not match bar.*.example.net)."
74- // RFC6125
75- if ( ! wildcards && / \* / . test ( host ) || / [ \. \* ] .* \* / . test ( host ) ||
76- / \* / . test ( host ) && ! / \* .* \. .+ \. .+ / . test ( host ) ) {
77- return / $ ./ ;
78- }
79-
80- // Replace wildcard chars with regexp's wildcard and
81- // escape all characters that have special meaning in regexps
82- // (i.e. '.', '[', '{', '*', and others)
83- var re = host . replace (
84- / \* ( [ a - z 0 - 9 \\ -_ \. ] ) | [ \. , \- \\ \^ \$ + ? * \[ \] \( \) : ! \| { } ] / g,
85- function ( all , sub ) {
86- if ( sub ) return '[a-z0-9\\-_]*' + ( sub === '-' ? '\\-' : sub ) ;
87- return '\\' + all ;
88- } ) ;
89-
90- return new RegExp ( '^' + re + '$' , 'i' ) ;
59+ function unfqdn ( host ) {
60+ return host . replace ( / [ . ] $ / , '' ) ;
61+ }
62+
63+ function splitHost ( host ) {
64+ // String#toLowerCase() is locale-sensitive so we use
65+ // a conservative version that only lowercases A-Z.
66+ const replacer = ( c ) => String . fromCharCode ( 32 + c . charCodeAt ( 0 ) ) ;
67+ return unfqdn ( host ) . replace ( / [ A - Z ] / g, replacer ) . split ( '.' ) ;
68+ }
69+
70+ function check ( hostParts , pattern , wildcards ) {
71+ // Empty strings, null, undefined, etc. never match.
72+ if ( ! pattern )
73+ return false ;
74+
75+ const patternParts = splitHost ( pattern ) ;
76+
77+ if ( hostParts . length !== patternParts . length )
78+ return false ;
79+
80+ // Pattern has empty components, e.g. "bad..example.com".
81+ if ( patternParts . indexOf ( '' ) !== - 1 )
82+ return false ;
83+
84+ // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no
85+ // good way to detect their encoding or normalize them so we simply
86+ // reject them. Control characters and blanks are rejected as well
87+ // because nothing good can come from accepting them.
88+ const isBad = ( s ) => / [ ^ \u0021 - \u007F ] / . test ( s ) ;
89+ if ( patternParts . some ( isBad ) )
90+ return false ;
91+
92+ // Check host parts from right to left first.
93+ for ( let i = hostParts . length - 1 ; i > 0 ; i -= 1 )
94+ if ( hostParts [ i ] !== patternParts [ i ] )
95+ return false ;
96+
97+ const hostSubdomain = hostParts [ 0 ] ;
98+ const patternSubdomain = patternParts [ 0 ] ;
99+ const patternSubdomainParts = patternSubdomain . split ( '*' ) ;
100+
101+ // Short-circuit when the subdomain does not contain a wildcard.
102+ // RFC 6125 does not allow wildcard substitution for components
103+ // containing IDNA A-labels (Punycode) so match those verbatim.
104+ if ( patternSubdomainParts . length === 1 ||
105+ patternSubdomain . indexOf ( 'xn--' ) !== - 1 ) {
106+ return hostSubdomain === patternSubdomain ;
91107 }
92108
93- var dnsNames = [ ] ;
94- var uriNames = [ ] ;
109+
110+ if ( ! wildcards )
111+ return false ;
112+
113+ // More than one wildcard is always wrong.
114+ if ( patternSubdomainParts . length > 2 )
115+ return false ;
116+
117+ // *.tld wildcards are not allowed.
118+ if ( patternParts . length <= 2 )
119+ return false ;
120+
121+ const prefix = patternSubdomainParts [ 0 ] ;
122+ const suffix = patternSubdomainParts [ 1 ] ;
123+
124+ if ( prefix . length + suffix . length > hostSubdomain . length )
125+ return false ;
126+
127+ if ( ! hostSubdomain . startsWith ( prefix ) )
128+ return false ;
129+
130+ if ( ! hostSubdomain . endsWith ( suffix ) )
131+ return false ;
132+
133+ return true ;
134+ }
135+
136+ exports . checkServerIdentity = function checkServerIdentity ( host , cert ) {
137+ const subject = cert . subject ;
138+ const altNames = cert . subjectaltname ;
139+ const dnsNames = [ ] ;
140+ const uriNames = [ ] ;
95141 const ips = [ ] ;
96- var matchCN = true ;
97- var valid = false ;
98- var reason = 'Unknown reason' ;
99-
100- // There're several names to perform check against:
101- // CN and altnames in certificate extension
102- // (DNS names, IP addresses, and URIs)
103- //
104- // Walk through altnames and generate lists of those names
105- if ( cert . subjectaltname ) {
106- cert . subjectaltname . split ( / , / g) . forEach ( function ( altname ) {
107- var option = altname . match ( / ^ ( D N S | I P A d d r e s s | U R I ) : ( .* ) $ / ) ;
108- if ( ! option )
109- return ;
110- if ( option [ 1 ] === 'DNS' ) {
111- dnsNames . push ( option [ 2 ] ) ;
112- } else if ( option [ 1 ] === 'IP Address' ) {
113- ips . push ( option [ 2 ] ) ;
114- } else if ( option [ 1 ] === 'URI' ) {
115- var uri = url . parse ( option [ 2 ] ) ;
116- if ( uri ) uriNames . push ( uri . hostname ) ;
117- }
118- } ) ;
119- }
120142
121- // If hostname is an IP address, it should be present in the list of IP
122- // addresses.
123- if ( net . isIP ( host ) ) {
124- valid = ips . some ( function ( ip ) {
125- return ip === host ;
126- } ) ;
127- if ( ! valid ) {
128- reason = `IP: ${ host } is not in the cert's list: ${ ips . join ( ', ' ) } ` ;
129- }
130- } else if ( cert . subject ) {
131- // Transform hostname to canonical form
132- if ( ! host || ! host . endsWith ( '.' ) ) host += '.' ;
133-
134- // Otherwise check all DNS/URI records from certificate
135- // (with allowed wildcards)
136- dnsNames = dnsNames . map ( function ( name ) {
137- return regexpify ( name , true ) ;
138- } ) ;
139-
140- // Wildcards ain't allowed in URI names
141- uriNames = uriNames . map ( function ( name ) {
142- return regexpify ( name , false ) ;
143- } ) ;
144-
145- dnsNames = dnsNames . concat ( uriNames ) ;
146-
147- if ( dnsNames . length > 0 ) matchCN = false ;
148-
149- // Match against Common Name (CN) only if no supported identifiers are
150- // present.
151- //
152- // "As noted, a client MUST NOT seek a match for a reference identifier
153- // of CN-ID if the presented identifiers include a DNS-ID, SRV-ID,
154- // URI-ID, or any application-specific identifier types supported by the
155- // client."
156- // RFC6125
157- if ( matchCN ) {
158- var commonNames = cert . subject . CN ;
159- if ( Array . isArray ( commonNames ) ) {
160- for ( var i = 0 , k = commonNames . length ; i < k ; ++ i ) {
161- dnsNames . push ( regexpify ( commonNames [ i ] , true ) ) ;
162- }
163- } else {
164- dnsNames . push ( regexpify ( commonNames , true ) ) ;
143+ host = '' + host ;
144+
145+ if ( altNames ) {
146+ for ( const name of altNames . split ( ', ' ) ) {
147+ if ( name . startsWith ( 'DNS:' ) ) {
148+ dnsNames . push ( name . slice ( 4 ) ) ;
149+ } else if ( name . startsWith ( 'URI:' ) ) {
150+ const uri = url . parse ( name . slice ( 4 ) ) ;
151+ uriNames . push ( uri . hostname ) ; // TODO(bnoordhuis) Also use scheme.
152+ } else if ( name . startsWith ( 'IP Address:' ) ) {
153+ ips . push ( name . slice ( 11 ) ) ;
165154 }
166155 }
156+ }
167157
168- valid = dnsNames . some ( function ( re ) {
169- return re . test ( host ) ;
170- } ) ;
158+ let valid = false ;
159+ let reason = 'Unknown reason' ;
171160
172- if ( ! valid ) {
173- if ( cert . subjectaltname ) {
174- reason =
175- `Host: ${ host } is not in the cert's altnames: ` +
176- `${ cert . subjectaltname } ` ;
177- } else {
178- reason = `Host: ${ host } is not cert's CN: ${ cert . subject . CN } ` ;
179- }
161+ if ( net . isIP ( host ) ) {
162+ valid = ips . indexOf ( host ) !== - 1 ;
163+ if ( ! valid )
164+ reason = `IP: ${ host } is not in the cert's list: ${ ips . join ( ', ' ) } ` ;
165+ // TODO(bnoordhuis) Also check URI SANs that are IP addresses.
166+ } else if ( subject ) {
167+ host = unfqdn ( host ) ; // Remove trailing dot for error messages.
168+ const hostParts = splitHost ( host ) ;
169+ const wildcard = ( pattern ) => check ( hostParts , pattern , true ) ;
170+ const noWildcard = ( pattern ) => check ( hostParts , pattern , false ) ;
171+
172+ // Match against Common Name only if no supported identifiers are present.
173+ if ( dnsNames . length === 0 && ips . length === 0 && uriNames . length === 0 ) {
174+ const cn = subject . CN ;
175+
176+ if ( Array . isArray ( cn ) )
177+ valid = cn . some ( wildcard ) ;
178+ else if ( cn )
179+ valid = wildcard ( cn ) ;
180+
181+ if ( ! valid )
182+ reason = `Host: ${ host } . is not cert's CN: ${ cn } ` ;
183+ } else {
184+ valid = dnsNames . some ( wildcard ) || uriNames . some ( noWildcard ) ;
185+ if ( ! valid )
186+ reason = `Host: ${ host } . is not in the cert's altnames: ${ altNames } ` ;
180187 }
181188 } else {
182189 reason = 'Cert is empty' ;
183190 }
184191
185192 if ( ! valid ) {
186- var err = new Error (
193+ const err = new Error (
187194 `Hostname/IP doesn't match certificate's altnames: "${ reason } "` ) ;
188195 err . reason = reason ;
189196 err . host = host ;
0 commit comments