@@ -7,10 +7,10 @@ use core::fmt;
77use core:: hash:: { self , Hash , Hasher } ;
88use core:: str:: Utf8Error ;
99
10- use nginx_sys:: { ngx_conf_t, ngx_http_server_name_t, ngx_str_t } ;
10+ use nginx_sys:: { ngx_conf_t, ngx_http_server_name_t} ;
1111use ngx:: allocator:: { AllocError , Allocator , TryCloneIn } ;
1212use ngx:: collections:: Vec ;
13- use ngx:: core:: { NgxString , Pool , Status } ;
13+ use ngx:: core:: { NgxString , Pool } ;
1414use ngx:: ngx_log_error;
1515use siphasher:: sip:: SipHasher ;
1616use thiserror:: Error ;
@@ -136,12 +136,10 @@ where
136136pub enum IdentifierError {
137137 #[ error( "memory allocation failed" ) ]
138138 Alloc ( #[ from] AllocError ) ,
139- #[ error( "invalid server name" ) ]
140- Invalid ,
139+ #[ error( "invalid server name: {0} " ) ]
140+ Invalid ( # [ from ] NameError ) ,
141141 #[ error( "invalid UTF-8 string" ) ]
142142 Utf8 ( #[ from] Utf8Error ) ,
143- #[ error( "unsupported wildcard server name" ) ]
144- Wildcard ,
145143}
146144
147145impl CertificateOrder < & ' static str , Pool > {
@@ -193,37 +191,39 @@ impl CertificateOrder<&'static str, Pool> {
193191 return self . push ( Identifier :: Ip ( addr) ) . map_err ( Into :: into) ;
194192 }
195193
196- if value. contains ( '*' ) {
197- return Err ( IdentifierError :: Wildcard ) ;
198- }
199-
200- let host = validate_host ( cf, value) . map_err ( |st| {
201- if st == Status :: NGX_ERROR {
202- IdentifierError :: Alloc ( AllocError )
203- } else {
204- IdentifierError :: Invalid
205- }
206- } ) ?;
194+ let realloc = validate_dns_identifier ( value) ?;
207195
208196 /*
209197 * The only special syntax we want to support is a leading dot, which matches the domain
210198 * with "www." and without it.
211199 * See <https://nginx.org/en/docs/http/server_names.html>
212200 */
213201
214- if let Some ( host) = host . strip_prefix ( "." ) {
202+ if let Some ( host) = value . strip_prefix ( "." ) {
215203 let mut www = Vec :: new_in ( self . identifiers . allocator ( ) . clone ( ) ) ;
216204 www. try_reserve_exact ( host. len ( ) + 4 )
217205 . map_err ( |_| AllocError ) ?;
218206 www. extend_from_slice ( b"www." ) ;
219207 www. extend_from_slice ( host. as_bytes ( ) ) ;
208+ www. make_ascii_lowercase ( ) ;
209+ // SAFETY: www is a pre-validated ASCII character slice.
220210 // The buffer is owned by ngx_pool_t and does not leak.
221- let www = core:: str:: from_utf8 ( www. leak ( ) ) ? ;
211+ let www = unsafe { core:: str:: from_utf8_unchecked ( www. leak ( ) ) } ;
222212
223213 self . push ( Identifier :: Dns ( www) ) ?;
224- self . push ( Identifier :: Dns ( host) ) ?;
214+ self . push ( Identifier :: Dns ( & www[ 4 ..] ) ) ?;
215+ } else if realloc {
216+ let mut out = Vec :: new_in ( self . identifiers . allocator ( ) . clone ( ) ) ;
217+ out. try_reserve_exact ( value. len ( ) ) . map_err ( |_| AllocError ) ?;
218+ out. extend_from_slice ( value. as_bytes ( ) ) ;
219+ out. make_ascii_lowercase ( ) ;
220+ // SAFETY: out is a pre-validated ASCII character slice.
221+ // The buffer is owned by ngx_pool_t and does not leak.
222+ let out = unsafe { core:: str:: from_utf8_unchecked ( out. leak ( ) ) } ;
223+
224+ self . push ( Identifier :: Dns ( out) ) ?;
225225 } else {
226- self . push ( Identifier :: Dns ( host ) ) ?;
226+ self . push ( Identifier :: Dns ( value ) ) ?;
227227 }
228228
229229 Ok ( ( ) )
@@ -289,17 +289,119 @@ fn parse_ip_identifier(
289289 Ok ( Some ( out) )
290290}
291291
292- /// Checks if the value is a valid domain name and returns a canonical (lowercase) form,
293- /// reallocated on the configuration pool if necessary.
294- fn validate_host ( cf : & ngx_conf_t , host : & ' static str ) -> Result < & ' static str , Status > {
295- let mut host = ngx_str_t {
296- data : host. as_ptr ( ) . cast_mut ( ) ,
297- len : host. len ( ) ,
298- } ;
299- let rc = Status ( unsafe { nginx_sys:: ngx_http_validate_host ( & mut host, cf. pool , 0 ) } ) ;
300- if rc != Status :: NGX_OK {
301- return Err ( rc) ;
292+ #[ derive( Debug , Error , PartialEq ) ]
293+ pub enum NameError {
294+ #[ error( "invalid character {0:x} at position {1}" ) ]
295+ Invalid ( u8 , usize ) ,
296+ #[ error( "unexpected character '{0}' at position {1}" ) ]
297+ Unexpected ( char , usize ) ,
298+ #[ error( "does not end with a label" ) ]
299+ NotALabel ,
300+ }
301+
302+ /// Validates that the `name` can be used as a DNS identifier.
303+ ///
304+ /// RFC8555 § 7.1.4 states that the "dns" identifier is a fully qualified domain name,
305+ /// encoded according to the rules in RFC5280 § 7.
306+ /// In practice, existing ACME servers and clients are rather relaxed about this requirement,
307+ /// trusting the user to specify valid values and the underlying protocol implementations (DNS or
308+ /// HTTP) to reject invalid ones.
309+ ///
310+ /// Given that, the validation logic is loosely based on the `ngx_http_validate_host` as of 1.29.4,
311+ /// asserting that the name
312+ ///
313+ /// - is a printable ASCII string, as required both by ACME and by NGINX[^1]
314+ /// - is a valid Host value accepted by NGINX
315+ /// - can have a leading dot or a single leading wildcard label
316+ /// - ends with a non-empty label
317+ ///
318+ /// Returns true if the name needs to be reallocated and converted to lower case.
319+ ///
320+ /// [^1]: https://nginx.org/en/docs/http/server_names.html
321+ fn validate_dns_identifier ( name : & str ) -> Result < bool , NameError > {
322+ #[ derive( PartialEq , Eq ) ]
323+ enum State {
324+ Start ,
325+ Label ,
326+ Dot ,
327+ Wildcard ,
328+ }
329+
330+ let mut alloc = false ;
331+ let mut state = State :: Start ;
332+
333+ for ( i, ch) in name. bytes ( ) . enumerate ( ) {
334+ state = match ch {
335+ // non-printable - handle separately for better error formatting
336+ 0x00 ..=0x20 | 0x7f .. => return Err ( NameError :: Invalid ( ch, i) ) ,
337+
338+ b'*' if state == State :: Start => State :: Wildcard ,
339+ b'.' if state != State :: Dot => State :: Dot ,
340+ // A wildcard domain name consists of a single asterisk character followed by a single
341+ // full stop character ("*.") followed by a domain name (RFC8555 § 7.1.3)
342+ _ if state == State :: Wildcard => return Err ( NameError :: Unexpected ( ch as _ , i) ) ,
343+
344+ // unreserved
345+ b'A' ..=b'Z' => {
346+ alloc = true ;
347+ State :: Label
348+ }
349+ b'0' ..=b'9' | b'a' ..=b'z' | b'-' | b'_' | b'~' => State :: Label ,
350+
351+ // pct-encoded
352+ b'%' => State :: Label ,
353+
354+ // sub-delims
355+ b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'+' | b',' | b';' | b'=' => State :: Label ,
356+
357+ _ => return Err ( NameError :: Unexpected ( ch as _ , i) ) ,
358+ } ;
302359 }
303360
304- unsafe { super :: conf_value_to_str ( & host) } . map_err ( |_| Status :: NGX_ERROR )
361+ // We intentionally reject the trailing dot, as it should not appear in the TLS layer.
362+
363+ if state != State :: Label {
364+ return Err ( NameError :: NotALabel ) ;
365+ }
366+
367+ Ok ( alloc)
368+ }
369+
370+ #[ cfg( test) ]
371+ mod tests {
372+ use super :: * ;
373+
374+ #[ test]
375+ fn test_validate_dns_identifier ( ) {
376+ for ( name, expect) in [
377+ ( "example" , Ok ( false ) ) ,
378+ ( "_example" , Ok ( false ) ) ,
379+ ( "exam_ple" , Ok ( false ) ) ,
380+ ( "Example" , Ok ( true ) ) ,
381+ ( "Example." , Err ( NameError :: NotALabel ) ) ,
382+ ( "E\x10 ample" , Err ( NameError :: Invalid ( 0x10 , 1 ) ) ) ,
383+ ( "00.example.test" , Ok ( false ) ) ,
384+ ( "www1.example.Test" , Ok ( true ) ) ,
385+ ( "www.example..test" , Err ( NameError :: Unexpected ( '.' , 12 ) ) ) ,
386+ ( "www.example.test." , Err ( NameError :: NotALabel ) ) ,
387+ ( "пример.испытание" , Err ( NameError :: Invalid ( 0xd0 , 0 ) ) ) ,
388+ ( "xn--e1afmkfd.xn--80akhbyknj4f" , Ok ( false ) ) ,
389+ // wildcards
390+ ( "*" , Err ( NameError :: NotALabel ) ) ,
391+ ( "*.example.test" , Ok ( false ) ) ,
392+ ( "*.xn--e1afmkfd.xn--80akhbyknj4f" , Ok ( false ) ) ,
393+ ( "*ww.example.test" , Err ( NameError :: Unexpected ( 'w' , 1 ) ) ) ,
394+ ( "ww*.example.test" , Err ( NameError :: Unexpected ( '*' , 2 ) ) ) ,
395+ ( "www.example.*" , Err ( NameError :: Unexpected ( '*' , 12 ) ) ) ,
396+ // incorrect syntax
397+ ( "" , Err ( NameError :: NotALabel ) ) ,
398+ ( "*." , Err ( NameError :: NotALabel ) ) ,
399+ ] {
400+ assert_eq ! (
401+ super :: validate_dns_identifier( name) ,
402+ expect,
403+ "incorrect validation result for \" {name}\" " ,
404+ ) ;
405+ }
406+ }
305407}
0 commit comments