diff --git a/security-framework-sys/src/secure_transport.rs b/security-framework-sys/src/secure_transport.rs index 828bf3f9..fd48f308 100644 --- a/security-framework-sys/src/secure_transport.rs +++ b/security-framework-sys/src/secure_transport.rs @@ -266,4 +266,6 @@ extern "C" { pub fn SSLSetALPNProtocols(context: SSLContextRef, protocols: CFArrayRef) -> OSStatus; #[cfg(feature = "OSX_10_13")] pub fn SSLCopyALPNProtocols(context: SSLContextRef, protocols: *mut CFArrayRef) -> OSStatus; + #[cfg(feature = "OSX_10_13")] + pub fn SSLSetSessionTicketsEnabled(context: SSLContextRef, enabled: Boolean) -> OSStatus; } diff --git a/security-framework/Cargo.toml b/security-framework/Cargo.toml index 0967d2d8..550e4506 100644 --- a/security-framework/Cargo.toml +++ b/security-framework/Cargo.toml @@ -24,12 +24,13 @@ hex = "0.4" [features] alpn = [] +session-tickets = [] OSX_10_9 = ["security-framework-sys/OSX_10_9"] OSX_10_10 = ["OSX_10_9", "security-framework-sys/OSX_10_10"] OSX_10_11 = ["OSX_10_10", "security-framework-sys/OSX_10_11"] OSX_10_12 = ["OSX_10_11", "security-framework-sys/OSX_10_12"] -OSX_10_13 = ["OSX_10_12", "security-framework-sys/OSX_10_13", "alpn"] +OSX_10_13 = ["OSX_10_12", "security-framework-sys/OSX_10_13", "alpn", "session-tickets"] nightly = [] diff --git a/security-framework/src/lib.rs b/security-framework/src/lib.rs index 3595b08a..46b5f504 100644 --- a/security-framework/src/lib.rs +++ b/security-framework/src/lib.rs @@ -35,7 +35,7 @@ macro_rules! p { }; } -#[cfg(all(not(feature = "OSX_10_13"), feature = "alpn"))] +#[cfg(all(not(feature = "OSX_10_13"), any(feature = "alpn", feature = "session-tickets")))] #[macro_use] mod dlsym; diff --git a/security-framework/src/secure_transport.rs b/security-framework/src/secure_transport.rs index d47ca760..d35ecd60 100644 --- a/security-framework/src/secure_transport.rs +++ b/security-framework/src/secure_transport.rs @@ -766,6 +766,30 @@ impl SslContext { } } + /// Sets whether the client sends the `SessionTicket` extension in its `ClientHello`. + /// + /// On its own, this will just cause the client to send an empty `SessionTicket` extension on + /// every connection. [`SslContext::set_peer_id`] must also be used to key the session + /// ticket returned by the server. + /// + /// [`SslContext::set_peer_id`]: #method.set_peer_id + #[cfg(feature = "session-tickets")] + pub fn set_session_tickets_enabled(&mut self, enabled: bool) -> Result<()> { + #[cfg(feature = "OSX_10_13")] + { + unsafe { cvt(SSLSetSessionTicketsEnabled(self.0, enabled as Boolean)) } + } + #[cfg(not(feature = "OSX_10_13"))] + { + dlsym! { fn SSLSetSessionTicketsEnabled(SSLContextRef, Boolean) -> OSStatus } + if let Some(f) = SSLSetSessionTicketsEnabled.get() { + unsafe { cvt(f(self.0, enabled as Boolean)) } + } else { + Err(Error::from_code(errSecUnimplemented)) + } + } + } + /// Sets whether a protocol is enabled or not. /// /// # Note @@ -1152,6 +1176,7 @@ pub struct ClientBuilder { whitelisted_ciphers: Vec, blacklisted_ciphers: Vec, alpn: Option>, + enable_session_tickets: bool, } impl Default for ClientBuilder { @@ -1176,6 +1201,7 @@ impl ClientBuilder { whitelisted_ciphers: Vec::new(), blacklisted_ciphers: Vec::new(), alpn: None, + enable_session_tickets: false, } } @@ -1265,6 +1291,15 @@ impl ClientBuilder { self } + /// Configures the use of the RFC 5077 `SessionTicket` extension. + /// + /// Defaults to `false`. + #[cfg(feature = "session-tickets")] + pub fn enable_session_tickets(&mut self, enable: bool) -> &mut Self { + self.enable_session_tickets = enable; + self + } + /// Initiates a new SSL/TLS session over a stream connected to the specified domain. /// /// If both SNI and hostname verification are disabled, the value of `domain` will be ignored. @@ -1316,6 +1351,15 @@ impl ClientBuilder { ctx.set_alpn_protocols(&alpn.iter().map(|s| &**s).collect::>())?; } } + #[cfg(feature = "session-tickets")] + { + if self.enable_session_tickets { + // We must use the domain here to ensure that we go through certificate validation + // again rather than resuming the session if the domain changes. + ctx.set_peer_id(domain.as_bytes())?; + ctx.set_session_tickets_enabled(true)?; + } + } ctx.set_break_on_server_auth(true)?; self.configure_protocols(&mut ctx)?; self.configure_ciphers(&mut ctx)?; @@ -1430,6 +1474,65 @@ mod test { println!("{}", String::from_utf8_lossy(&buf)); } + #[test] + fn client_no_session_ticket_resumption() { + for _ in 0..2 { + let stream = p!(TcpStream::connect("google.com:443")); + + // Manually handshake here. + let stream = MidHandshakeSslStream { + stream: ClientBuilder::new() + .ctx_into_stream("google.com", stream) + .unwrap(), + error: Error::from(errSecSuccess), + }; + + let mut result = stream.handshake(); + + if let Err(HandshakeError::Interrupted(stream)) = result { + assert!(stream.server_auth_completed()); + result = stream.handshake(); + } else { + panic!("Unexpectedly skipped server auth"); + } + + assert!(result.is_ok()); + } + } + + #[test] + #[cfg(feature = "session-tickets")] + fn client_session_ticket_resumption() { + // The first time through this loop, we should do a full handshake. The second time, we + // should immediately finish the handshake without breaking on server auth. + for i in 0..2 { + let stream = p!(TcpStream::connect("google.com:443")); + let mut builder = ClientBuilder::new(); + builder.enable_session_tickets(true); + + // Manually handshake here. + let stream = MidHandshakeSslStream { + stream: builder.ctx_into_stream("google.com", stream).unwrap(), + error: Error::from(errSecSuccess), + }; + + let mut result = stream.handshake(); + + if let Err(HandshakeError::Interrupted(stream)) = result { + assert!(stream.server_auth_completed()); + assert_eq!( + i, 0, + "Session ticket resumption did not work, server auth was not skipped" + ); + result = stream.handshake(); + } else { + assert_eq!(i, 1, "Unexpectedly skipped server auth"); + } + + assert!(result.is_ok()); + } + } + #[test] #[cfg(feature = "alpn")] fn client_alpn_accept() {