@@ -56,134 +56,141 @@ exports.convertNPNProtocols = function convertNPNProtocols(NPNProtocols, out) {
56
56
}
57
57
} ;
58
58
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 ;
91
107
}
92
108
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 = [ ] ;
95
141
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
- }
120
142
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 ) ) ;
165
154
}
166
155
}
156
+ }
167
157
168
- valid = dnsNames . some ( function ( re ) {
169
- return re . test ( host ) ;
170
- } ) ;
158
+ let valid = false ;
159
+ let reason = 'Unknown reason' ;
171
160
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 } ` ;
180
187
}
181
188
} else {
182
189
reason = 'Cert is empty' ;
183
190
}
184
191
185
192
if ( ! valid ) {
186
- var err = new Error (
193
+ const err = new Error (
187
194
`Hostname/IP doesn't match certificate's altnames: "${ reason } "` ) ;
188
195
err . reason = reason ;
189
196
err . host = host ;
0 commit comments