diff --git a/openssl-sys/Cargo.toml b/openssl-sys/Cargo.toml index f3fbfc18..bc9e9876 100644 --- a/openssl-sys/Cargo.toml +++ b/openssl-sys/Cargo.toml @@ -19,6 +19,7 @@ dtlsv1_2 = [] sslv2 = [] aes_xts = [] npn = [] +alpn = [] [dependencies] libc = "0.1" diff --git a/openssl-sys/src/lib.rs b/openssl-sys/src/lib.rs index b347b949..9f2041a4 100644 --- a/openssl-sys/src/lib.rs +++ b/openssl-sys/src/lib.rs @@ -547,13 +547,31 @@ extern "C" { inlen: c_uint, arg: *mut c_void) -> c_int, arg: *mut c_void); - #[cfg(feature = "npn")] + #[cfg(any(feature = "alpn", feature = "npn"))] pub fn SSL_select_next_proto(out: *mut *mut c_uchar, outlen: *mut c_uchar, inbuf: *const c_uchar, inlen: c_uint, client: *const c_uchar, client_len: c_uint) -> c_int; #[cfg(feature = "npn")] pub fn SSL_get0_next_proto_negotiated(s: *const SSL, data: *mut *const c_uchar, len: *mut c_uint); + #[cfg(feature = "alpn")] + pub fn SSL_CTX_set_alpn_protos(s: *mut SSL_CTX, data: *const c_uchar, len: c_uint) -> c_int; + + #[cfg(feature = "alpn")] + pub fn SSL_set_alpn_protos(s: *mut SSL, data: *const c_uchar, len: c_uint) -> c_int; + + #[cfg(feature = "alpn")] + pub fn SSL_CTX_set_alpn_select_cb(ssl: *mut SSL_CTX, + cb: extern "C" fn(ssl: *mut SSL, + out: *mut *mut c_uchar, + outlen: *mut c_uchar, + inbuf: *const c_uchar, + inlen: c_uint, + arg: *mut c_void) -> c_int, + arg: *mut c_void); + #[cfg(feature = "alpn")] + pub fn SSL_get0_alpn_selected(s: *const SSL, data: *mut *const c_uchar, len: *mut c_uint); + pub fn X509_add_ext(x: *mut X509, ext: *mut X509_EXTENSION, loc: c_int) -> c_int; pub fn X509_digest(x: *mut X509, digest: *const EVP_MD, buf: *mut c_char, len: *mut c_uint) -> c_int; pub fn X509_free(x: *mut X509); diff --git a/openssl/Cargo.toml b/openssl/Cargo.toml index c2097774..3e3c34de 100644 --- a/openssl/Cargo.toml +++ b/openssl/Cargo.toml @@ -17,6 +17,7 @@ dtlsv1_2 = ["openssl-sys/dtlsv1_2"] sslv2 = ["openssl-sys/sslv2"] aes_xts = ["openssl-sys/aes_xts"] npn = ["openssl-sys/npn"] +alpn = ["openssl-sys/alpn"] [dependencies.openssl-sys] path = "../openssl-sys" diff --git a/openssl/src/ssl/mod.rs b/openssl/src/ssl/mod.rs index 67ecd302..d67dc1a2 100644 --- a/openssl/src/ssl/mod.rs +++ b/openssl/src/ssl/mod.rs @@ -13,9 +13,9 @@ use std::sync::{Once, ONCE_INIT, Arc, Mutex}; use std::ops::{Deref, DerefMut}; use std::cmp; use std::any::Any; -#[cfg(feature = "npn")] +#[cfg(any(feature = "npn", feature = "alpn"))] use libc::{c_uchar, c_uint}; -#[cfg(feature = "npn")] +#[cfg(any(feature = "npn", feature = "alpn"))] use std::slice; use bio::{MemBio}; @@ -179,6 +179,10 @@ fn get_verify_data_idx() -> c_int { lazy_static! { static ref NPN_PROTOS_IDX: c_int = get_new_idx::>(); } +#[cfg(feature = "alpn")] +lazy_static! { + static ref ALPN_PROTOS_IDX: c_int = get_new_idx::>(); +} /// Determine a new index to use for SSL CTX ex data. /// Registers a destruct for the data which will be called by openssl when the context is freed. @@ -248,6 +252,26 @@ extern fn raw_verify_with_data(preverify_ok: c_int, } } +#[cfg(any(feature = "npn", feature = "alpn"))] +unsafe fn select_proto_using(ssl: *mut ffi::SSL, + out: *mut *mut c_uchar, outlen: *mut c_uchar, + inbuf: *const c_uchar, inlen: c_uint, + ex_data: c_int) -> c_int { + + // First, get the list of protocols (that the client should support) saved in the context + // extra data. + let ssl_ctx = ffi::SSL_get_SSL_CTX(ssl); + let protocols = ffi::SSL_CTX_get_ex_data(ssl_ctx, ex_data); + let protocols: &Vec = mem::transmute(protocols); + // Prepare the client list parameters to be passed to the OpenSSL function... + let client = protocols.as_ptr(); + let client_len = protocols.len() as c_uint; + // Finally, let OpenSSL find a protocol to be used, by matching the given server and + // client lists. + ffi::SSL_select_next_proto(out, outlen, inbuf, inlen, client, client_len); + ffi::SSL_TLSEXT_ERR_OK +} + /// The function is given as the callback to `SSL_CTX_set_next_proto_select_cb`. /// /// It chooses the protocol that the client wishes to use, out of the given list of protocols @@ -260,20 +284,18 @@ extern fn raw_next_proto_select_cb(ssl: *mut ffi::SSL, inbuf: *const c_uchar, inlen: c_uint, _arg: *mut c_void) -> c_int { unsafe { - // First, get the list of protocols (that the client should support) saved in the context - // extra data. - let ssl_ctx = ffi::SSL_get_SSL_CTX(ssl); - let protocols = ffi::SSL_CTX_get_ex_data(ssl_ctx, *NPN_PROTOS_IDX); - let protocols: &Vec = mem::transmute(protocols); - // Prepare the client list parameters to be passed to the OpenSSL function... - let client = protocols.as_ptr(); - let client_len = protocols.len() as c_uint; - // Finally, let OpenSSL find a protocol to be used, by matching the given server and - // client lists. - ffi::SSL_select_next_proto(out, outlen, inbuf, inlen, client, client_len); + select_proto_using(ssl, out, outlen, inbuf, inlen, *NPN_PROTOS_IDX) } +} - ffi::SSL_TLSEXT_ERR_OK +#[cfg(feature = "alpn")] +extern fn raw_alpn_select_cb(ssl: *mut ffi::SSL, + out: *mut *mut c_uchar, outlen: *mut c_uchar, + inbuf: *const c_uchar, inlen: c_uint, + _arg: *mut c_void) -> c_int { + unsafe { + select_proto_using(ssl, out, outlen, inbuf, inlen, *ALPN_PROTOS_IDX) + } } /// The function is given as the callback to `SSL_CTX_set_next_protos_advertised_cb`. @@ -308,7 +330,7 @@ extern fn raw_next_protos_advertise_cb(ssl: *mut ffi::SSL, /// Convert a set of byte slices into a series of byte strings encoded for SSL. Encoding is a byte /// containing the length followed by the string. -#[cfg(feature = "npn")] +#[cfg(any(feature = "npn", feature = "alpn"))] fn ssl_encode_byte_strings(strings: &[&[u8]]) -> Vec { let mut enc = Vec::new(); @@ -549,6 +571,35 @@ impl SslContext { ffi::SSL_CTX_set_next_protos_advertised_cb(self.ctx, raw_next_protos_advertise_cb, ptr::null_mut()); } } + + /// Set the protocols to be used during ALPN (application layer protocol negotiation). + /// If this is a server, these are the protocols we report to the client. + /// If this is a client, these are the protocols we try to match with those reported by the + /// server. + /// + /// Note that ordering of the protocols controls the priority with which they are chosen. + /// + /// This method needs the `alpn` feature. + #[cfg(feature = "alpn")] + pub fn set_alpn_protocols(&mut self, protocols: &[&[u8]]) { + let protocols: Box> = Box::new(ssl_encode_byte_strings(protocols)); + unsafe { + // Set the context's internal protocol list for use if we are a server + ffi::SSL_CTX_set_alpn_protos(self.ctx, protocols.as_ptr(), protocols.len() as c_uint); + + // Rather than use the argument to the callback to contain our data, store it in the + // ssl ctx's ex_data so that we can configure a function to free it later. In the + // future, it might make sense to pull this into our internal struct Ssl instead of + // leaning on openssl and using function pointers. + ffi::SSL_CTX_set_ex_data(self.ctx, *ALPN_PROTOS_IDX, + mem::transmute(protocols)); + + // Now register the callback that performs the default protocol + // matching based on the client-supported list of protocols that + // has been saved. + ffi::SSL_CTX_set_alpn_select_cb(self.ctx, raw_alpn_select_cb, ptr::null_mut()); + } + } } #[allow(dead_code)] @@ -690,6 +741,29 @@ impl Ssl { } } + /// Returns the protocol selected by performing ALPN, if any. + /// + /// The protocol's name is returned is an opaque sequence of bytes. It is up to the client + /// to interpret it. + /// + /// This method needs the `alpn` feature. + #[cfg(feature = "alpn")] + pub fn get_selected_alpn_protocol(&self) -> Option<&[u8]> { + unsafe { + let mut data: *const c_uchar = ptr::null(); + let mut len: c_uint = 0; + // Get the negotiated protocol from the SSL instance. + // `data` will point at a `c_uchar` array; `len` will contain the length of this array. + ffi::SSL_get0_alpn_selected(self.ssl, &mut data, &mut len); + + if data.is_null() { + None + } else { + Some(slice::from_raw_parts(data, len as usize)) + } + } + } + /// pending() takes into account only bytes from the TLS/SSL record that is currently being processed (if any). pub fn pending(&self) -> usize { unsafe { @@ -1171,6 +1245,17 @@ impl SslStream { self.kind.ssl().get_selected_npn_protocol() } + /// Returns the protocol selected by performing ALPN, if any. + /// + /// The protocol's name is returned is an opaque sequence of bytes. It is up to the client + /// to interpret it. + /// + /// This method needs the `alpn` feature. + #[cfg(feature = "alpn")] + pub fn get_selected_alpn_protocol(&self) -> Option<&[u8]> { + self.ssl.get_selected_alpn_protocol() + } + /// pending() takes into account only bytes from the TLS/SSL record that is currently being processed (if any). pub fn pending(&self) -> usize { self.kind.ssl().pending() diff --git a/openssl/src/ssl/tests.rs b/openssl/src/ssl/tests.rs index c4673edc..b44b9c35 100644 --- a/openssl/src/ssl/tests.rs +++ b/openssl/src/ssl/tests.rs @@ -390,6 +390,28 @@ fn test_pending() { assert_eq!(pending, len); } +/// Tests that connecting with the client using NPN, but the server not does not +/// break the existing connection behavior. +#[test] +#[cfg(feature = "alpn")] +fn test_connect_with_unilateral_alpn() { + let stream = TcpStream::connect("127.0.0.1:15418").unwrap(); + let mut ctx = SslContext::new(Sslv23).unwrap(); + ctx.set_verify(SSL_VERIFY_PEER, None); + ctx.set_alpn_protocols(&[b"http/1.1", b"spdy/3.1"]); + match ctx.set_CA_file(&Path::new("test/cert.pem")) { + Ok(_) => {} + Err(err) => panic!("Unexpected error {:?}", err) + } + let stream = match SslStream::new(&ctx, stream) { + Ok(stream) => stream, + Err(err) => panic!("Expected success, got {:?}", err) + }; + // Since the socket to which we connected is not configured to use NPN, + // there should be no selected protocol... + assert!(stream.get_selected_alpn_protocol().is_none()); +} + /// Tests that connecting with the client using NPN, but the server not does not /// break the existing connection behavior. #[test] @@ -412,6 +434,30 @@ fn test_connect_with_unilateral_npn() { assert!(stream.get_selected_npn_protocol().is_none()); } +/// Tests that when both the client as well as the server use ALPN and their +/// lists of supported protocols have an overlap, the correct protocol is chosen. +#[test] +#[cfg(feature = "alpn")] +fn test_connect_with_alpn_successful_multiple_matching() { + // A different port than the other tests: an `openssl` process that has + // NPN enabled. + let stream = TcpStream::connect("127.0.0.1:15419").unwrap(); + let mut ctx = SslContext::new(Sslv23).unwrap(); + ctx.set_verify(SSL_VERIFY_PEER, None); + ctx.set_alpn_protocols(&[b"spdy/3.1", b"http/1.1"]); + match ctx.set_CA_file(&Path::new("test/cert.pem")) { + Ok(_) => {} + Err(err) => panic!("Unexpected error {:?}", err) + } + let stream = match SslStream::new(&ctx, stream) { + Ok(stream) => stream, + Err(err) => panic!("Expected success, got {:?}", err) + }; + // The server prefers "http/1.1", so that is chosen, even though the client + // would prefer "spdy/3.1" + assert_eq!(b"http/1.1", stream.get_selected_alpn_protocol().unwrap()); +} + /// Tests that when both the client as well as the server use NPN and their /// lists of supported protocols have an overlap, the correct protocol is chosen. #[test] @@ -436,6 +482,32 @@ fn test_connect_with_npn_successful_multiple_matching() { assert_eq!(b"http/1.1", stream.get_selected_npn_protocol().unwrap()); } +/// Tests that when both the client as well as the server use ALPN and their +/// lists of supported protocols have an overlap -- with only ONE protocol +/// being valid for both. +#[test] +#[cfg(feature = "alpn")] +fn test_connect_with_alpn_successful_single_match() { + // A different port than the other tests: an `openssl` process that has + // ALPN enabled. + let stream = TcpStream::connect("127.0.0.1:15419").unwrap(); + let mut ctx = SslContext::new(Sslv23).unwrap(); + ctx.set_verify(SSL_VERIFY_PEER, None); + ctx.set_alpn_protocols(&[b"spdy/3.1"]); + match ctx.set_CA_file(&Path::new("test/cert.pem")) { + Ok(_) => {} + Err(err) => panic!("Unexpected error {:?}", err) + } + let stream = match SslStream::new(&ctx, stream) { + Ok(stream) => stream, + Err(err) => panic!("Expected success, got {:?}", err) + }; + // The client now only supports one of the server's protocols, so that one + // is used. + assert_eq!(b"spdy/3.1", stream.get_selected_alpn_protocol().unwrap()); +} + + /// Tests that when both the client as well as the server use NPN and their /// lists of supported protocols have an overlap -- with only ONE protocol /// being valid for both. @@ -502,6 +574,47 @@ fn test_npn_server_advertise_multiple() { assert_eq!(b"spdy/3.1", stream.get_selected_npn_protocol().unwrap()); } +/// Tests that when the `SslStream` is created as a server stream, the protocols +/// are correctly advertised to the client. +#[test] +#[cfg(feature = "alpn")] +fn test_alpn_server_advertise_multiple() { + let localhost = "127.0.0.1:15420"; + let listener = TcpListener::bind(localhost).unwrap(); + // We create a different context instance for the server... + let listener_ctx = { + let mut ctx = SslContext::new(Sslv23).unwrap(); + ctx.set_verify(SSL_VERIFY_PEER, None); + ctx.set_alpn_protocols(&[b"http/1.1", b"spdy/3.1"]); + assert!(ctx.set_certificate_file( + &Path::new("test/cert.pem"), X509FileType::PEM).is_ok()); + ctx.set_private_key_file( + &Path::new("test/key.pem"), X509FileType::PEM).unwrap(); + ctx + }; + // Have the listener wait on the connection in a different thread. + thread::spawn(move || { + let (stream, _) = listener.accept().unwrap(); + let _ = SslStream::new_server(&listener_ctx, stream).unwrap(); + }); + + let mut ctx = SslContext::new(Sslv23).unwrap(); + ctx.set_verify(SSL_VERIFY_PEER, None); + ctx.set_alpn_protocols(&[b"spdy/3.1"]); + match ctx.set_CA_file(&Path::new("test/cert.pem")) { + Ok(_) => {} + Err(err) => panic!("Unexpected error {:?}", err) + } + // Now connect to the socket and make sure the protocol negotiation works... + let stream = TcpStream::connect(localhost).unwrap(); + let stream = match SslStream::new(&ctx, stream) { + Ok(stream) => stream, + Err(err) => panic!("Expected success, got {:?}", err) + }; + // SPDY is selected since that's the only thing the client supports. + assert_eq!(b"spdy/3.1", stream.get_selected_alpn_protocol().unwrap()); +} + #[cfg(feature="dtlsv1")] #[cfg(test)] mod dtlsv1 { diff --git a/openssl/test/test.sh b/openssl/test/test.sh index 975b124e..9beb37c3 100755 --- a/openssl/test/test.sh +++ b/openssl/test/test.sh @@ -4,7 +4,7 @@ cd $(dirname $0) openssl s_server -accept 15418 -www -cert cert.pem -key key.pem >/dev/null 2>&1 & openssl s_server -accept 15419 -www -cert cert.pem -key key.pem \ - -nextprotoneg "http/1.1,spdy/3.1" >/dev/null 2>&1 & + -nextprotoneg "http/1.1,spdy/3.1" -alpn "http/1.1,spdy/3.1" >/dev/null 2>&1 & openssl s_server -no_ssl2 -accept 15420 -www -cert cert.pem -key key.pem >/dev/null 2>&1 & if test "$TRAVIS_OS_NAME" == "osx"; then