From 48a48d2bb4c3d8966473e4120f22e2a061a32dbb Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sat, 14 Oct 2023 12:58:45 -0700 Subject: [PATCH] expose DANE functions for SSL/SSL_CTX The docs at https://www.openssl.org/docs/man1.1.1/man3/SSL_dane_clear_flags.html indicate that DANE functionality has been available since version 1.1.0 --- openssl-sys/src/handwritten/ssl.rs | 39 +++ openssl-sys/src/x509_vfy.rs | 1 + openssl/src/ssl/mod.rs | 397 ++++++++++++++++++++++++++++- openssl/src/ssl/test/mod.rs | 54 ++++ 4 files changed, 489 insertions(+), 2 deletions(-) diff --git a/openssl-sys/src/handwritten/ssl.rs b/openssl-sys/src/handwritten/ssl.rs index 944a476618..7267073759 100644 --- a/openssl-sys/src/handwritten/ssl.rs +++ b/openssl-sys/src/handwritten/ssl.rs @@ -938,3 +938,42 @@ extern "C" { #[cfg(any(ossl110, libressl360))] pub fn SSL_get_security_level(s: *const SSL) -> c_int; } + +#[cfg(ossl110)] +extern "C" { + pub fn SSL_CTX_dane_enable(ctx: *mut SSL_CTX) -> c_int; + pub fn SSL_CTX_dane_mtype_set( + ctx: *mut SSL_CTX, + md: *const EVP_MD, + mtype: u8, + ord: u8, + ) -> c_int; + pub fn SSL_dane_enable(s: *mut SSL, basedomain: *const c_char) -> c_int; + pub fn SSL_dane_tlsa_add( + s: *mut SSL, + usage: u8, + selector: u8, + mtype: u8, + data: *const c_uchar, + dlen: size_t, + ) -> c_int; + pub fn SSL_get0_dane_authority( + s: *mut SSL, + mcert: *mut *mut X509, + mspki: *mut *mut EVP_PKEY, + ) -> c_int; + pub fn SSL_get0_dane_tlsa( + s: *mut SSL, + usage: *mut u8, + selector: *mut u8, + mtype: *mut u8, + data: *mut *const c_uchar, + dlen: *mut size_t, + ) -> c_int; + + pub fn SSL_CTX_dane_set_flags(ctx: *mut SSL_CTX, flags: c_ulong) -> c_ulong; + pub fn SSL_CTX_dane_clear_flags(ctx: *mut SSL_CTX, flags: c_ulong) -> c_ulong; + pub fn SSL_dane_set_flags(ssl: *mut SSL, flags: c_ulong) -> c_ulong; + pub fn SSL_dane_clear_flags(ssl: *mut SSL, flags: c_ulong) -> c_ulong; + pub fn SSL_add1_host(s: *mut SSL, hostname: *const c_char) -> c_int; +} diff --git a/openssl-sys/src/x509_vfy.rs b/openssl-sys/src/x509_vfy.rs index 2fa176fed5..91fb077103 100644 --- a/openssl-sys/src/x509_vfy.rs +++ b/openssl-sys/src/x509_vfy.rs @@ -91,6 +91,7 @@ cfg_if! { pub const X509_V_ERR_INVALID_CALL: c_int = 69; pub const X509_V_ERR_STORE_LOOKUP: c_int = 70; pub const X509_V_ERR_NO_VALID_SCTS: c_int = 71; + pub const DANE_FLAG_NO_DANE_EE_NAMECHECKS: c_ulong = 1; } else if #[cfg(ossl102h)] { pub const X509_V_ERR_INVALID_CALL: c_int = 65; pub const X509_V_ERR_STORE_LOOKUP: c_int = 66; diff --git a/openssl/src/ssl/mod.rs b/openssl/src/ssl/mod.rs index fb38bb3e4a..48d54d3ca9 100644 --- a/openssl/src/ssl/mod.rs +++ b/openssl/src/ssl/mod.rs @@ -69,9 +69,9 @@ use crate::ex_data::Index; use crate::hash::MessageDigest; #[cfg(any(ossl110, libressl270))] use crate::nid::Nid; -use crate::pkey::{HasPrivate, PKeyRef, Params, Private}; +use crate::pkey::{HasPrivate, PKeyRef, Params, Private, Public}; #[cfg(ossl300)] -use crate::pkey::{PKey, Public}; +use crate::pkey::{PKey}; use crate::srtp::{SrtpProtectionProfile, SrtpProtectionProfileRef}; use crate::ssl::bio::BioMethod; use crate::ssl::callbacks::*; @@ -1750,6 +1750,93 @@ impl SslContextBuilder { unsafe { ffi::SSL_CTX_set_security_level(self.as_ptr(), level as c_int) } } + /// initialize the shared state required for DANE support. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_CTX_dane_enable)] + #[cfg(ossl110)] + pub fn dane_enable(&mut self) -> Result<(), ErrorStack> { + unsafe { cvt(ffi::SSL_CTX_dane_enable(self.as_ptr())).map(|_| ()) } + } + + /// Adjust the supported digest algorithms. + /// This must be done before any SSL handles are created for the context. + /// + /// The mtype argument specifies a DANE TLSA matching type and the md argument + /// specifies the associated digest algorithm handle. + /// The ord argument specifies a strength ordinal. + /// + /// Algorithms with a larger strength ordinal are considered more secure. + /// + /// Strength ordinals are used to implement RFC7671 digest algorithm agility. + /// + /// Specifying None for the digest algorithm for a matching type disables + /// support for that matching type. + /// + /// Matching type Full(0) cannot be modified or disabled. + /// + /// By default, matching type SHA2-256(1) (see RFC7218 for definitions of + /// the DANE TLSA parameter acronyms) is mapped to EVP_sha256() with a strength + /// ordinal of 1 and matching type SHA2-512(2) is mapped to EVP_sha512() + /// with a strength ordinal of 2. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_CTX_dane_mtype_set)] + #[cfg(ossl110)] + pub fn dane_mtype_set( + &mut self, + md: Option, + 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.as_raw(), + ord, + )) + .map(|_| ()) + } + } + + /// 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_CTX_dane_set_flags)] + #[cfg(ossl110)] + pub fn set_no_dane_ee_namechecks(&mut self) { + unsafe { + ffi::SSL_CTX_dane_set_flags(self.as_ptr(), ffi::DANE_FLAG_NO_DANE_EE_NAMECHECKS); + } + } + + /// Enable server name checks when authenticating via DANE-EE(3) TLSA + /// records. + /// + /// This is the default state of the context. + /// + /// See `set_no_dane_ee_namechecks` for more information. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_CTX_dane_set_flags)] + #[cfg(ossl110)] + pub fn set_dane_ee_namechecks(&mut self) { + unsafe { + ffi::SSL_CTX_dane_clear_flags(self.as_ptr(), ffi::DANE_FLAG_NO_DANE_EE_NAMECHECKS); + } + } + /// Consumes the builder, returning a new `SslContext`. pub fn build(self) -> SslContext { self.0 @@ -2353,6 +2440,128 @@ impl Ssl { } } +/// Represents a TLSA selector as defined by +/// +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct DaneSelector(u8); + +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); + + /// Constructs a `DaneSelector` from a raw OpenSSL value. + pub fn from_raw(value: u8) -> Self { + Self(value) + } + + /// Returns the raw OpenSSL value represented by this type. + pub fn as_raw(&self) -> u8 { + self.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) + } + + /// Returns the raw OpenSSL value represented by this type. + pub fn as_raw(&self) -> u8 { + self.0 + } +} + +/// 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) + } + + /// Returns the raw OpenSSL value represented by this type. + pub fn as_raw(&self) -> u8 { + self.0 + } +} + +/// Returns information about the matched DANE trust-anchor +pub struct DaneAuthority<'a> { + /// If a TLSA record matched a chain certificate, this holds + /// that certificate + pub cert: Option<&'a X509Ref>, + + /// If no TLSA records directly matched any elements of the certificate chain, + /// but a DANE-TA(2) SPKI(1) Full(0) record provided the public key that signed + /// an element of the chain, then that key is returned here + pub pkey: Option<&'a PKeyRef>, + + /// The match depth. + /// 0 if an EE TLSA record directly matched the leaf certificate, or a positive + /// number indicating the depth at which a TA record matched an issuer certificate. + pub depth: usize, +} + +/// Represents the fields of the TLSA DNS record that matched the peer +/// certificate chain when DANE verification was successful. +#[derive(Debug, PartialEq, Eq)] +pub struct DaneTlsaUsed<'a> { + pub usage: DaneUsage, + pub selector: DaneSelector, + pub mtype: DaneMatchType, + + /// The binary data in wire form + pub data: &'a [u8], + + /// The match depth + pub depth: usize, +} + impl fmt::Debug for SslRef { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.debug_struct("Ssl") @@ -3503,6 +3712,190 @@ impl SslRef { } } } + + /// 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 set_no_dane_ee_namechecks(&mut self) { + unsafe { + ffi::SSL_dane_set_flags(self.as_ptr(), ffi::DANE_FLAG_NO_DANE_EE_NAMECHECKS); + } + } + + /// Enable server name checks when authenticating via DANE-EE(3) TLSA + /// records. + /// + /// This is the default state of the context. + /// + /// See `set_no_dane_ee_namechecks` for more information. + /// + /// Requires OpenSSL 1.1.0 or newer. + #[corresponds(SSL_dane_set_flags)] + #[cfg(ossl110)] + pub fn set_dane_ee_namechecks(&mut self) { + unsafe { + ffi::SSL_dane_clear_flags(self.as_ptr(), ffi::DANE_FLAG_NO_DANE_EE_NAMECHECKS); + } + } + + /// 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 412c4a5dc6..110baef9cc 100644 --- a/openssl/src/ssl/test/mod.rs +++ b/openssl/src/ssl/test/mod.rs @@ -752,6 +752,60 @@ fn connector_valid_hostname() { s.read_exact(&mut [0]).unwrap(); } +#[test] +#[cfg(ossl110)] +fn connector_dane() { + let server = Server::builder().build(); + + let mut connector = SslConnector::builder(SslMethod::tls()).unwrap(); + + connector.dane_enable().unwrap(); + connector.set_no_dane_ee_namechecks(); + let mut config = connector.build().configure().unwrap(); + + // The name in the cert is foobar.com, but we're claiming + // to access it via some other name. Since we turned off + // dane-ee-namechecks above, we expect this to still validate + // overall because the tlsa record matches the digest of the + // cert; the name is ignored. + config.dane_enable("mx.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("mx.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();