From 8ba816a7f9986b0c3813ecc8d586cac85b9c6371 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sat, 14 Oct 2023 21:13:53 -0700 Subject: [PATCH] improve typing of TLSA record fields, add test --- openssl/src/ssl/mod.rs | 436 ++++++++++++++++++++++-------------- openssl/src/ssl/test/mod.rs | 46 ++++ 2 files changed, 309 insertions(+), 173 deletions(-) diff --git a/openssl/src/ssl/mod.rs b/openssl/src/ssl/mod.rs index 9b30f31622..b13ed4b5b4 100644 --- a/openssl/src/ssl/mod.rs +++ b/openssl/src/ssl/mod.rs @@ -1754,14 +1754,14 @@ impl SslContextBuilder { pub fn dane_mtype_set( &mut self, md: Option, - mtype: u8, + mtype: DaneMatchType, ord: u8, ) -> Result<(), ErrorStack> { unsafe { cvt(ffi::SSL_CTX_dane_mtype_set( self.as_ptr(), md.map(|md| md.as_ptr()).unwrap_or(std::ptr::null_mut()), - mtype, + mtype.as_raw(), ord, )) .map(|_| ()) @@ -2392,185 +2392,95 @@ impl Ssl { { SslStreamBuilder::new(self, stream).accept() } +} - /// Enable optional DANE verification features. - /// - /// Currently, the only supported feature is `DANE_FLAG_NO_DANE_EE_NAMECHECKS` - /// which can be used to disable server name checks when authenticating via DANE-EE(3) TLSA - /// records. For some applications, primarily web browsers, it is not safe to disable name - /// checks due to "unknown key share" attacks, in which a malicious server can convince a - /// client that a connection to a victim server is instead a secure connection to the malicious - /// server. The malicious server may then be able to violate cross-origin scripting - /// restrictions. Thus, despite the text of RFC7671, name checks are by default enabled for - /// DANE-EE(3) TLSA records, and can be disabled in applications where it is safe to do so. In - /// particular, SMTP and XMPP clients should set this option as SRV and MX records already make - /// it possible for a remote domain to redirect client connections to any server of its choice, - /// and in any case SMTP and XMPP clients do not execute scripts downloaded from remote - /// servers. - /// - /// Requires OpenSSL 1.1.0 or newer. - #[corresponds(SSL_dane_set_flags)] - #[cfg(ossl110)] - pub fn dane_set_flags(&mut self, flags: libc::c_ulong) -> libc::c_ulong { - unsafe { ffi::SSL_dane_set_flags(self.as_ptr(), flags) } - } +/// Represents a TLSA selector as defined by +/// +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct DaneSelector(u8); - /// Disable optional DANE verification features. - /// - /// See `dane_set_flags` for more information. - /// - /// Requires OpenSSL 1.1.0 or newer. - #[corresponds(SSL_dane_clear_flags)] - #[cfg(ossl110)] - pub fn dane_clear_flags(&mut self, flags: libc::c_ulong) -> libc::c_ulong { - unsafe { ffi::SSL_dane_clear_flags(self.as_ptr(), flags) } - } +impl DaneSelector { + // These constants are not defined in the openssl sources, + // but are defined by the DANE RFCs, so the numeric values + // are embedded here directly. + // + /// Full Certificate + pub const CERT: DaneSelector = DaneSelector(0); + /// SubjectPublicKeyInfo + pub const SPKI: DaneSelector = DaneSelector(1); + /// Reserved for Private Use + pub const PRIV_SEL: DaneSelector = DaneSelector(255); - /// adds name as an additional reference identifier that can match the peer's certificate. Any - /// previous names set via SSL_set1_host() or SSL_add1_host() are retained, no change is made - /// if name is NULL or empty. When multiple names are configured, the peer is considered - /// verified when any name matches. This function is required for DANE TLSA in the presence of - /// service name indirection via CNAME, MX or SRV records as specified in RFC7671, RFC7672 or - /// RFC7673. - /// - /// Requires OpenSSL 1.1.0 or newer. - #[corresponds(SSL_add1_host)] - #[cfg(ossl110)] - pub fn add1_host(&mut self, hostname: &str) -> Result<(), ErrorStack> { - let cstr = CString::new(hostname).unwrap(); - unsafe { cvt(ffi::SSL_add1_host(self.as_ptr(), cstr.as_ptr())).map(|_| ()) } + /// Constructs a `DaneSelector` from a raw OpenSSL value. + pub fn from_raw(value: u8) -> Self { + Self(value) } - /// must be called before the SSL handshake is initiated with SSL_connect(3) - /// if (and only if) you want to enable DANE for that connection. - /// - /// The connection must be associated with a DANE-enabled SSL context. - /// - /// The base_domain argument specifies the RFC7671 TLSA base domain, - /// which will be the primary peer reference identifier for certificate name checks. - /// - /// Additional server names can be specified via `add1_host`. - /// - /// The base_domain is used as the default SNI hint if none has yet been - /// specified via SSL_set_tlsext_host_name(3). - /// - /// Requires OpenSSL 1.1.0 or newer. - #[corresponds(SSL_dane_enable)] - #[cfg(ossl110)] - pub fn dane_enable(&mut self, base_domain: &str) -> Result<(), ErrorStack> { - let cstr = CString::new(base_domain).unwrap(); - unsafe { cvt(ffi::SSL_dane_enable(self.as_ptr(), cstr.as_ptr())).map(|_| ()) } + /// Returns the raw OpenSSL value represented by this type. + pub fn as_raw(&self) -> u8 { + self.0 } +} - /// may be called one or more times to load each of the TLSA records that apply - /// to the remote TLS peer. - /// - /// This must be done prior to the beginning of the SSL handshake. - /// - /// The arguments specify the fields of the TLSA record. - /// The data field is provided in binary (wire RDATA) form, - /// not the hexadecimal ASCII presentation form. - /// - /// Returns a boolean to indicate whether the record was usable or not. - /// - /// Requires OpenSSL 1.1.0 or newer. - #[corresponds(SSL_dane_tlsa_add)] - #[cfg(ossl110)] - pub fn dane_tlsa_add( - &mut self, - usage: u8, - selector: u8, - mtype: u8, - data: &[u8], - ) -> Result { - let usable = unsafe { - cvt_n(ffi::SSL_dane_tlsa_add( - self.as_ptr(), - usage, - selector, - mtype, - data.as_ptr() as *const c_uchar, - data.len(), - )) - }?; - - Ok(usable > 0) +/// Represents a TLSA Certificate Usage as defined by +/// +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct DaneUsage(u8); + +impl DaneUsage { + // These constants are not defined in the openssl sources, + // but are defined by the DANE RFCs, so the numeric values + // are embedded here directly. + // + /// CA Constraint + pub const PKIX_TA: DaneUsage = DaneUsage(0); + /// Service certificate constraint + pub const PKIX_EE: DaneUsage = DaneUsage(1); + /// Trust anchor assertion + pub const DANE_TA: DaneUsage = DaneUsage(2); + /// Domain-issued certificate + pub const DANE_EE: DaneUsage = DaneUsage(3); + /// Reserved for private use + pub const PRIV_CERT: DaneUsage = DaneUsage(255); + + /// Constructs a `DaneUsage` from a raw OpenSSL value. + pub fn from_raw(value: u8) -> Self { + Self(value) } - /// get more detailed information about the matched DANE trust-anchor after - /// successful connection completion. - /// - /// Returns an error if DANE verification failed or was not enabled. - /// - /// Returns a DaneAuthority struct on success. - /// - /// The complete verified chain can be retrieved via `verified_chain`. - /// The `depth` field is an index into this verified chain. - /// - /// Requires OpenSSL 1.1.0 or newer. - #[corresponds(SSL_get0_dane_authority)] - #[cfg(ossl110)] - pub fn dane_authority(&self) -> Result, ErrorStack> { - let mut mcert = std::ptr::null_mut(); - let mut mspki = std::ptr::null_mut(); - let depth = unsafe { - cvt_n(ffi::SSL_get0_dane_authority( - self.as_ptr(), - &mut mcert, - &mut mspki, - ))? - } as usize; - - let cert = if mcert.is_null() { - None - } else { - unsafe { Some(X509Ref::from_ptr(mcert)) } - }; - - let pkey = if mspki.is_null() { - None - } else { - unsafe { PKeyRef::from_const_ptr_opt(mspki) } - }; - - Ok(DaneAuthority { depth, cert, pkey }) + /// Returns the raw OpenSSL value represented by this type. + pub fn as_raw(&self) -> u8 { + self.0 } +} - /// retrieve the fields of the TLSA record that matched the peer certificate chain. - /// - /// Returns an error if DANE verification failed or was not enabled. - /// - /// Returns a DaneTlsaUsed struct on success; the fields are populated with - /// the information from the TLSA record that matched the peer certificate chain. - /// - /// Requires OpenSSL 1.1.0 or newer. - #[corresponds(SSL_get0_dane_tlsa)] - #[cfg(ossl110)] - pub fn dane_tlsa(&self) -> Result, ErrorStack> { - let mut usage = 0; - let mut selector = 0; - let mut mtype = 0; - let mut data = std::ptr::null(); - let mut dlen = 0; - - let depth = unsafe { - cvt_n(ffi::SSL_get0_dane_tlsa( - self.as_ptr(), - &mut usage, - &mut selector, - &mut mtype, - &mut data, - &mut dlen, - ))? - } as usize; +/// Represents a TLSA matching type as defined by +/// +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct DaneMatchType(u8); + +impl DaneMatchType { + // These constants are not defined in the openssl sources, + // but are defined by the DANE RFCs, so the numeric values + // are embedded here directly. + // + /// No hash used + pub const FULL: DaneMatchType = DaneMatchType(0); + /// 256 bit hash by SHA2 + pub const SHA2_256: DaneMatchType = DaneMatchType(1); + /// 512 bit hash by SHA2 + pub const SHA2_512: DaneMatchType = DaneMatchType(2); + /// Reserved for private use + pub const PRIV_MATCH: DaneMatchType = DaneMatchType(255); + + /// Constructs a `DaneMatchType` from a raw OpenSSL value. + pub fn from_raw(value: u8) -> Self { + Self(value) + } - Ok(DaneTlsaUsed { - usage, - selector, - mtype, - depth, - data: unsafe { std::slice::from_raw_parts(data, dlen) }, - }) + /// Returns the raw OpenSSL value represented by this type. + pub fn as_raw(&self) -> u8 { + self.0 } } @@ -2593,11 +2503,11 @@ pub struct DaneAuthority<'a> { /// Represents the fields of the TLSA DNS record that matched the peer /// certificate chain when DANE verification was successful. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct DaneTlsaUsed<'a> { - pub usage: u8, - pub selector: u8, - pub mtype: u8, + pub usage: DaneUsage, + pub selector: DaneSelector, + pub mtype: DaneMatchType, /// The binary data in wire form pub data: &'a [u8], @@ -3700,6 +3610,186 @@ impl SslRef { pub fn num_tickets(&self) -> usize { unsafe { ffi::SSL_get_num_tickets(self.as_ptr()) } } + + /// Enable optional DANE verification features. + /// + /// Currently, the only supported feature is `DANE_FLAG_NO_DANE_EE_NAMECHECKS` + /// which can be used to disable server name checks when authenticating via DANE-EE(3) TLSA + /// records. For some applications, primarily web browsers, it is not safe to disable name + /// checks due to "unknown key share" attacks, in which a malicious server can convince a + /// client that a connection to a victim server is instead a secure connection to the malicious + /// server. The malicious server may then be able to violate cross-origin scripting + /// restrictions. Thus, despite the text of RFC7671, name checks are by default enabled for + /// DANE-EE(3) TLSA records, and can be disabled in applications where it is safe to do so. In + /// particular, SMTP and XMPP clients should set this option as SRV and MX records already make + /// it possible for a remote domain to redirect client connections to any server of its choice, + /// and in any case SMTP and XMPP clients do not execute scripts downloaded from remote + /// servers. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_dane_set_flags)] + #[cfg(ossl110)] + pub fn dane_set_flags(&mut self, flags: libc::c_ulong) -> libc::c_ulong { + unsafe { ffi::SSL_dane_set_flags(self.as_ptr(), flags) } + } + + /// Disable optional DANE verification features. + /// + /// See `dane_set_flags` for more information. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_dane_clear_flags)] + #[cfg(ossl110)] + pub fn dane_clear_flags(&mut self, flags: libc::c_ulong) -> libc::c_ulong { + unsafe { ffi::SSL_dane_clear_flags(self.as_ptr(), flags) } + } + + /// Adds name as an additional reference identifier that can match the peer's certificate. Any + /// previous names set via SSL_set1_host() or SSL_add1_host() are retained, no change is made + /// if name is NULL or empty. When multiple names are configured, the peer is considered + /// verified when any name matches. This function is required for DANE TLSA in the presence of + /// service name indirection via CNAME, MX or SRV records as specified in RFC7671, RFC7672 or + /// RFC7673. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_add1_host)] + #[cfg(ossl110)] + pub fn add1_host(&mut self, hostname: &str) -> Result<(), ErrorStack> { + let cstr = CString::new(hostname).unwrap(); + unsafe { cvt(ffi::SSL_add1_host(self.as_ptr(), cstr.as_ptr())).map(|_| ()) } + } + + /// Must be called before the SSL handshake is initiated with SSL_connect(3) + /// if (and only if) you want to enable DANE for that connection. + /// + /// The connection must be associated with a DANE-enabled SSL context. + /// + /// The base_domain argument specifies the RFC7671 TLSA base domain, + /// which will be the primary peer reference identifier for certificate name checks. + /// + /// Additional server names can be specified via `add1_host`. + /// + /// The base_domain is used as the default SNI hint if none has yet been + /// specified via SSL_set_tlsext_host_name(3). + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_dane_enable)] + #[cfg(ossl110)] + pub fn dane_enable(&mut self, base_domain: &str) -> Result<(), ErrorStack> { + let cstr = CString::new(base_domain).unwrap(); + unsafe { cvt(ffi::SSL_dane_enable(self.as_ptr(), cstr.as_ptr())).map(|_| ()) } + } + + /// May be called one or more times to load each of the TLSA records that apply + /// to the remote TLS peer. + /// + /// This must be done prior to the beginning of the SSL handshake. + /// + /// The arguments specify the fields of the TLSA record. + /// The data field is provided in binary (wire RDATA) form, + /// not the hexadecimal ASCII presentation form. + /// + /// Returns a boolean to indicate whether the record was usable or not. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_dane_tlsa_add)] + #[cfg(ossl110)] + pub fn dane_tlsa_add( + &mut self, + usage: DaneUsage, + selector: DaneSelector, + mtype: DaneMatchType, + data: &[u8], + ) -> Result { + let usable = unsafe { + cvt_n(ffi::SSL_dane_tlsa_add( + self.as_ptr(), + usage.as_raw(), + selector.as_raw(), + mtype.as_raw(), + data.as_ptr() as *const c_uchar, + data.len(), + )) + }?; + + Ok(usable > 0) + } + + /// Get more detailed information about the matched DANE trust-anchor after + /// successful connection completion. + /// + /// Returns an error if DANE verification failed or was not enabled. + /// + /// Returns a DaneAuthority struct on success. + /// + /// The complete verified chain can be retrieved via `verified_chain`. + /// The `depth` field is an index into this verified chain. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_get0_dane_authority)] + #[cfg(ossl110)] + pub fn dane_authority(&self) -> Result, ErrorStack> { + let mut mcert = std::ptr::null_mut(); + let mut mspki = std::ptr::null_mut(); + let depth = unsafe { + cvt_n(ffi::SSL_get0_dane_authority( + self.as_ptr(), + &mut mcert, + &mut mspki, + ))? + } as usize; + + let cert = if mcert.is_null() { + None + } else { + unsafe { Some(X509Ref::from_ptr(mcert)) } + }; + + let pkey = if mspki.is_null() { + None + } else { + unsafe { PKeyRef::from_const_ptr_opt(mspki) } + }; + + Ok(DaneAuthority { depth, cert, pkey }) + } + + /// Retrieve the fields of the TLSA record that matched the peer certificate chain. + /// + /// Returns an error if DANE verification failed or was not enabled. + /// + /// Returns a DaneTlsaUsed struct on success; the fields are populated with + /// the information from the TLSA record that matched the peer certificate chain. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_get0_dane_tlsa)] + #[cfg(ossl110)] + pub fn dane_tlsa(&self) -> Result, ErrorStack> { + let mut usage = 0; + let mut selector = 0; + let mut mtype = 0; + let mut data = std::ptr::null(); + let mut dlen = 0; + + let depth = unsafe { + cvt_n(ffi::SSL_get0_dane_tlsa( + self.as_ptr(), + &mut usage, + &mut selector, + &mut mtype, + &mut data, + &mut dlen, + ))? + } as usize; + + Ok(DaneTlsaUsed { + usage: DaneUsage::from_raw(usage), + selector: DaneSelector::from_raw(selector), + mtype: DaneMatchType::from_raw(mtype), + depth, + data: unsafe { std::slice::from_raw_parts(data, dlen) }, + }) + } } /// An SSL stream midway through the handshake process. diff --git a/openssl/src/ssl/test/mod.rs b/openssl/src/ssl/test/mod.rs index 7707af238f..c2a6f235de 100644 --- a/openssl/src/ssl/test/mod.rs +++ b/openssl/src/ssl/test/mod.rs @@ -702,6 +702,52 @@ fn connector_valid_hostname() { s.read_exact(&mut [0]).unwrap(); } +#[test] +fn connector_dane() { + let server = Server::builder().build(); + + let mut connector = SslConnector::builder(SslMethod::tls()).unwrap(); + + connector.dane_enable().unwrap(); + let mut config = connector.build().configure().unwrap(); + config.dane_enable("foobar.com").unwrap(); + + let cert = X509::from_pem(CERT).unwrap(); + let data = cert.digest(MessageDigest::sha256()).unwrap(); + + let usable = config + .dane_tlsa_add( + crate::ssl::DaneUsage::DANE_EE, + crate::ssl::DaneSelector::CERT, + crate::ssl::DaneMatchType::SHA2_256, + &data, + ) + .unwrap(); + + assert!(usable); + + let s = server.connect_tcp(); + let mut s = config.connect("foobar.com", s).unwrap(); + s.read_exact(&mut [0]).unwrap(); + + let authority = s.ssl.dane_authority().unwrap(); + assert!(authority.cert.unwrap() == &cert); + assert_eq!(authority.depth, 0); + assert!(authority.pkey.is_none()); + + let tlsa = s.ssl.dane_tlsa().unwrap(); + assert_eq!( + tlsa, + crate::ssl::DaneTlsaUsed { + usage: crate::ssl::DaneUsage::DANE_EE, + selector: crate::ssl::DaneSelector::CERT, + mtype: crate::ssl::DaneMatchType::SHA2_256, + data: &data, + depth: 0, + } + ); +} + #[test] fn connector_invalid_hostname() { let mut server = Server::builder();