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