feat(boring): adapt `boring2` for compio async runtime (#85)

close: https://github.com/0x676e67/boring2/issues/78
This commit is contained in:
0x676e67 2025-07-07 21:10:50 +08:00 committed by GitHub
parent 736c374e3d
commit 83e049d8d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 496 additions and 3 deletions

View File

@ -141,6 +141,7 @@ jobs:
rust: stable
os: ubuntu-latest
apt_packages: gcc-multilib g++-multilib
extra_test_args: --workspace --exclude compio-boring2
- thing: arm-linux
target: arm-unknown-linux-gnueabi
rust: stable
@ -151,6 +152,7 @@ jobs:
CC: arm-linux-gnueabi-gcc
CXX: arm-linux-gnueabi-g++
CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABI_LINKER: arm-linux-gnueabi-g++
extra_test_args: --workspace --exclude compio-boring2
- thing: aarch64-linux
target: aarch64-unknown-linux-gnu
rust: stable
@ -182,19 +184,19 @@ jobs:
CPLUS_INCLUDE_PATH: "C:\\msys64\\usr\\include"
LIBRARY_PATH: "C:\\msys64\\usr\\lib"
# CI's Windows doesn't have required root certs
extra_test_args: --workspace --exclude tokio-boring2 --exclude hyper-boring2
extra_test_args: --workspace --exclude tokio-boring2 --exclude compio-boring2
- thing: i686-msvc
target: i686-pc-windows-msvc
rust: stable-x86_64-msvc
os: windows-latest
# CI's Windows doesn't have required root certs
extra_test_args: --workspace --exclude tokio-boring2 --exclude hyper-boring2
extra_test_args: --workspace --exclude tokio-boring2 --exclude compio-boring2
- thing: x86_64-msvc
target: x86_64-pc-windows-msvc
rust: stable-x86_64-msvc
os: windows-latest
# CI's Windows doesn't have required root certs
extra_test_args: --workspace --exclude tokio-boring2 --exclude hyper-boring2
extra_test_args: --workspace --exclude tokio-boring2 --exclude compio-boring2
steps:
- uses: actions/checkout@v4

View File

@ -3,6 +3,7 @@ members = [
"boring",
"boring-sys",
"tokio-boring",
"compio-boring",
]
resolver = "2"
@ -21,6 +22,7 @@ publish = false
boring-sys = { package = "boring-sys2", version = "5.0.0-alpha.3", path = "./boring-sys" }
boring = { package = "boring2", version = "5.0.0-alpha.3", path = "./boring" }
tokio-boring = { package = "tokio-boring2", version = "5.0.0-alpha.3", path = "./tokio-boring" }
compio-boring = { package = "compio-boring2", version = "0.1.0-alpha.1", path = "./compio-boring" }
bindgen = { version = "0.72.0", default-features = false, features = ["runtime"] }
bytes = "1"
@ -40,3 +42,5 @@ linked_hash_set = "0.1"
openssl-macros = "0.1.1"
autocfg = "1.3.0"
brotli = "8"
compio = { version = "0.15.0" }
compio-io = { version = "0.7.0" }

View File

@ -17,6 +17,7 @@ This package implements only the TLS extensions specification and supports the o
## Documentation
- Boring API: <https://docs.rs/boring2>
- tokio TLS adapters: <https://docs.rs/tokio-boring2>
- compio TLS adapters: <https://docs.rs/compio-boring2>
- FFI bindings: <https://docs.rs/boring-sys2>
## Contribution

46
compio-boring/Cargo.toml Normal file
View File

@ -0,0 +1,46 @@
[package]
name = "compio-boring2"
version = { workspace = true }
authors = ["0x676e67 <gngppz@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = { workspace = true }
repository = { workspace = true }
documentation = "https://docs.rs/compio-boring2"
description = """
An implementation of SSL streams for Compio backed by BoringSSL
"""
[package.metadata.docs.rs]
features = ["pq-experimental"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
# Use a FIPS-validated version of boringssl.
fips = ["boring/fips", "boring-sys/fips"]
# Use a FIPS build of BoringSSL, but don't set "fips-compat".
#
# As of boringSSL commit a430310d6563c0734ddafca7731570dfb683dc19, we no longer
# need to make exceptions for the types of BufLen, ProtosLen, and ValueLen,
# which means the "fips-compat" feature is no longer needed.
#
# TODO(cjpatton) Delete this feature and modify "fips" so that it doesn't imply
# "fips-compat".
fips-precompiled = ["boring/fips-precompiled"]
# Link with precompiled FIPS-validated `bcm.o` module.
fips-link-precompiled = ["boring/fips-link-precompiled", "boring-sys/fips-link-precompiled"]
# Enables experimental post-quantum crypto (https://blog.cloudflare.com/post-quantum-for-all/)
pq-experimental = ["boring/pq-experimental"]
[dependencies]
boring = { workspace = true }
boring-sys = { workspace = true }
compio = { workspace = true }
compio-io = { workspace = true, features = ["compat"]}
[dev-dependencies]
futures = { workspace = true }
compio = { workspace = true, features = [ "macros"] }
anyhow = { workspace = true }

View File

@ -0,0 +1,77 @@
use boring::ssl::{
AsyncPrivateKeyMethod, AsyncSelectCertError, BoxGetSessionFuture, BoxSelectCertFuture,
ClientHello, SslContextBuilder, SslRef,
};
/// Extensions to [`SslContextBuilder`].
///
/// This trait provides additional methods to use async callbacks with boring.
pub trait SslContextBuilderExt: private::Sealed {
/// Sets a callback that is called before most [`ClientHello`] processing
/// and before the decision whether to resume a session is made. The
/// callback may inspect the [`ClientHello`] and configure the connection.
///
/// This method uses a function that returns a future whose output is
/// itself a closure that will be passed [`ClientHello`] to configure
/// the connection based on the computations done in the future.
///
/// See [`SslContextBuilder::set_select_certificate_callback`] for the sync
/// setter of this callback.
fn set_async_select_certificate_callback<F>(&mut self, callback: F)
where
F: Fn(&mut ClientHello<'_>) -> Result<BoxSelectCertFuture, AsyncSelectCertError>
+ Send
+ Sync
+ 'static;
/// Configures a custom private key method on the context.
///
/// See [`AsyncPrivateKeyMethod`] for more details.
fn set_async_private_key_method(&mut self, method: impl AsyncPrivateKeyMethod);
/// Sets a callback that is called when a client proposed to resume a session
/// but it was not found in the internal cache.
///
/// The callback is passed a reference to the session ID provided by the client.
/// It should return the session corresponding to that ID if available. This is
/// only used for servers, not clients.
///
/// See [`SslContextBuilder::set_get_session_callback`] for the sync setter
/// of this callback.
///
/// # Safety
///
/// The returned [`SslSession`] must not be associated with a different [`SslContext`].
unsafe fn set_async_get_session_callback<F>(&mut self, callback: F)
where
F: Fn(&mut SslRef, &[u8]) -> Option<BoxGetSessionFuture> + Send + Sync + 'static;
}
impl SslContextBuilderExt for SslContextBuilder {
fn set_async_select_certificate_callback<F>(&mut self, callback: F)
where
F: Fn(&mut ClientHello<'_>) -> Result<BoxSelectCertFuture, AsyncSelectCertError>
+ Send
+ Sync
+ 'static,
{
self.set_async_select_certificate_callback(callback);
}
fn set_async_private_key_method(&mut self, method: impl AsyncPrivateKeyMethod) {
self.set_async_private_key_method(method);
}
unsafe fn set_async_get_session_callback<F>(&mut self, callback: F)
where
F: Fn(&mut SslRef, &[u8]) -> Option<BoxGetSessionFuture> + Send + Sync + 'static,
{
self.set_async_get_session_callback(callback);
}
}
mod private {
pub trait Sealed {}
}
impl private::Sealed for SslContextBuilder {}

331
compio-boring/src/lib.rs Normal file
View File

@ -0,0 +1,331 @@
//! Async TLS streams backed by BoringSSL
//!
//! This library is an implementation of TLS streams using BoringSSL for
//! negotiating the connection. Each TLS stream implements the `Read` and
//! `Write` traits to interact and interoperate with the rest of the futures I/O
//! ecosystem. Client connections initiated from this crate verify hostnames
//! automatically and by default.
//!
//! `tokio-boring` exports this ability through [`accept`] and [`connect`]. `accept` should
//! be used by servers, and `connect` by clients. These augment the functionality provided by the
//! [`boring`] crate, on which this crate is built. Configuration of TLS parameters is still
//! primarily done through the [`boring`] crate.
#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
mod async_callbacks;
use boring::ssl::{self, ConnectConfiguration, ErrorCode, SslAcceptor, SslRef};
use boring_sys as ffi;
use compio::buf::{IoBuf, IoBufMut};
use compio::io::{AsyncRead, AsyncWrite};
use compio::BufResult;
use compio_io::compat::SyncStream;
use std::error::Error;
use std::fmt;
use std::io;
use std::mem::MaybeUninit;
pub use crate::async_callbacks::SslContextBuilderExt;
pub use boring::ssl::{
AsyncPrivateKeyMethod, AsyncPrivateKeyMethodError, AsyncSelectCertError, BoxGetSessionFinish,
BoxGetSessionFuture, BoxPrivateKeyMethodFinish, BoxPrivateKeyMethodFuture, BoxSelectCertFinish,
BoxSelectCertFuture, ExDataFuture,
};
/// Asynchronously performs a client-side TLS handshake over the provided stream.
///
/// This function automatically sets the task waker on the `Ssl` from `config` to
/// allow to make use of async callbacks provided by the boring crate.
pub async fn connect<S>(
config: ConnectConfiguration,
domain: &str,
stream: S,
) -> Result<SslStream<S>, HandshakeError<S>>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let res = config.connect(domain, SyncStream::new(stream));
perform_tls_handshake(res).await
}
/// Asynchronously performs a server-side TLS handshake over the provided stream.
///
/// This function automatically sets the task waker on the `Ssl` from `config` to
/// allow to make use of async callbacks provided by the boring crate.
pub async fn accept<S>(acceptor: &SslAcceptor, stream: S) -> Result<SslStream<S>, HandshakeError<S>>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let res = acceptor.accept(SyncStream::new(stream));
perform_tls_handshake(res).await
}
/// A partially constructed `SslStream`, useful for unusual handshakes.
pub struct SslStreamBuilder<S> {
inner: ssl::SslStreamBuilder<SyncStream<S>>,
}
impl<S> SslStreamBuilder<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
/// Begins creating an `SslStream` atop `stream`.
pub fn new(ssl: ssl::Ssl, stream: S) -> Self {
Self {
inner: ssl::SslStreamBuilder::new(ssl, SyncStream::new(stream)),
}
}
/// Initiates a client-side TLS handshake.
pub async fn accept(self) -> Result<SslStream<S>, HandshakeError<S>> {
let res = self.inner.connect();
perform_tls_handshake(res).await
}
/// Initiates a server-side TLS handshake.
pub async fn connect(self) -> Result<SslStream<S>, HandshakeError<S>> {
let res = self.inner.connect();
perform_tls_handshake(res).await
}
}
impl<S> SslStreamBuilder<S> {
/// Returns a shared reference to the `Ssl` object associated with this builder.
#[must_use]
pub fn ssl(&self) -> &SslRef {
self.inner.ssl()
}
/// Returns a mutable reference to the `Ssl` object associated with this builder.
pub fn ssl_mut(&mut self) -> &mut SslRef {
self.inner.ssl_mut()
}
}
/// A wrapper around an underlying raw stream which implements the SSL
/// protocol.
///
/// A `SslStream<S>` represents a handshake that has been completed successfully
/// and both the server and the client are ready for receiving and sending
/// data. Bytes read from a `SslStream` are decrypted from `S` and bytes written
/// to a `SslStream` are encrypted when passing through to `S`.
#[derive(Debug)]
pub struct SslStream<S>(ssl::SslStream<SyncStream<S>>);
impl<S> SslStream<S> {
/// Returns a shared reference to the `Ssl` object associated with this stream.
#[must_use]
pub fn ssl(&self) -> &SslRef {
self.0.ssl()
}
/// Returns a mutable reference to the `Ssl` object associated with this stream.
pub fn ssl_mut(&mut self) -> &mut SslRef {
self.0.ssl_mut()
}
/// Returns a shared reference to the underlying stream.
#[must_use]
pub fn get_ref(&self) -> &S {
self.0.get_ref().get_ref()
}
/// Returns a mutable reference to the underlying stream.
pub fn get_mut(&mut self) -> &mut S {
self.0.get_mut().get_mut()
}
}
impl<S> SslStream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
/// Constructs an `SslStream` from a pointer to the underlying OpenSSL `SSL` struct.
///
/// This is useful if the handshake has already been completed elsewhere.
///
/// # Safety
///
/// The caller must ensure the pointer is valid.
pub unsafe fn from_raw_parts(ssl: *mut ffi::SSL, stream: S) -> Self {
Self(ssl::SslStream::from_raw_parts(ssl, SyncStream::new(stream)))
}
}
impl<S: AsyncRead> AsyncRead for SslStream<S> {
async fn read<B: IoBufMut>(&mut self, mut buf: B) -> BufResult<usize, B> {
let slice = buf.as_mut_slice();
let mut f = {
slice.fill(MaybeUninit::new(0));
// SAFETY: The memory has been initialized
let slice =
unsafe { std::slice::from_raw_parts_mut(slice.as_mut_ptr().cast(), slice.len()) };
|s: &mut _| std::io::Read::read(s, slice)
};
loop {
match f(&mut self.0) {
Ok(res) => {
unsafe { buf.set_buf_init(res) };
return BufResult(Ok(res), buf);
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
match self.0.get_mut().fill_read_buf().await {
Ok(_) => continue,
Err(e) => return BufResult(Err(e), buf),
}
}
res => return BufResult(res, buf),
}
}
}
// OpenSSL does not support vectored reads
}
/// `AsyncRead` is needed for shutting down stream.
impl<S: AsyncWrite + AsyncRead> AsyncWrite for SslStream<S> {
async fn write<T: IoBuf>(&mut self, buf: T) -> BufResult<usize, T> {
let slice = buf.as_slice();
loop {
let res = io::Write::write(&mut self.0, slice);
match res {
Err(e) if e.kind() == io::ErrorKind::WouldBlock => match self.flush().await {
Ok(_) => continue,
Err(e) => return BufResult(Err(e), buf),
},
_ => return BufResult(res, buf),
}
}
}
async fn flush(&mut self) -> io::Result<()> {
loop {
match io::Write::flush(&mut self.0) {
Ok(()) => break,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
self.0.get_mut().flush_write_buf().await?;
}
Err(e) => return Err(e),
}
}
self.0.get_mut().flush_write_buf().await?;
Ok(())
}
async fn shutdown(&mut self) -> io::Result<()> {
self.flush().await?;
self.0.get_mut().get_mut().shutdown().await
}
}
/// The error type returned after a failed handshake.
pub enum HandshakeError<S> {
/// An error that occurred during the handshake.
Inner(ssl::HandshakeError<SyncStream<S>>),
/// An I/O error that occurred during the handshake.
Io(io::Error),
}
impl<S> HandshakeError<S> {
/// Returns a shared reference to the `Ssl` object associated with this error.
#[must_use]
pub fn ssl(&self) -> Option<&SslRef> {
match self {
HandshakeError::Inner(ssl::HandshakeError::Failure(s)) => Some(s.ssl()),
_ => None,
}
}
/// Returns the error code, if any.
#[must_use]
pub fn code(&self) -> Option<ErrorCode> {
match self {
HandshakeError::Inner(ssl::HandshakeError::Failure(s)) => Some(s.error().code()),
_ => None,
}
}
/// Returns a reference to the inner I/O error, if any.
#[must_use]
pub fn as_io_error(&self) -> Option<&io::Error> {
match self {
HandshakeError::Inner(ssl::HandshakeError::Failure(s)) => s.error().io_error(),
HandshakeError::Io(e) => Some(e),
_ => None,
}
}
}
impl<S> fmt::Debug for HandshakeError<S>
where
S: fmt::Debug,
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HandshakeError::Inner(e) => fmt::Debug::fmt(e, fmt),
HandshakeError::Io(e) => fmt::Debug::fmt(e, fmt),
}
}
}
impl<S> fmt::Display for HandshakeError<S> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HandshakeError::Inner(e) => fmt::Display::fmt(e, fmt),
HandshakeError::Io(e) => fmt::Display::fmt(e, fmt),
}
}
}
impl<S> Error for HandshakeError<S>
where
S: fmt::Debug,
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
HandshakeError::Inner(e) => e.source(),
HandshakeError::Io(e) => Some(e),
}
}
}
async fn perform_tls_handshake<S: AsyncRead + AsyncWrite>(
mut res: Result<ssl::SslStream<SyncStream<S>>, ssl::HandshakeError<SyncStream<S>>>,
) -> Result<SslStream<S>, HandshakeError<S>> {
loop {
match res {
Ok(mut s) => {
s.get_mut()
.flush_write_buf()
.await
.map_err(HandshakeError::Io)?;
return Ok(SslStream(s));
}
Err(e) => match e {
ssl::HandshakeError::Failure(_) => return Err(HandshakeError::Inner(e)),
ssl::HandshakeError::SetupFailure(_) => {
return Err(HandshakeError::Inner(e));
}
ssl::HandshakeError::WouldBlock(mut mid_stream) => {
if mid_stream
.get_mut()
.flush_write_buf()
.await
.map_err(HandshakeError::Io)?
== 0
{
mid_stream
.get_mut()
.fill_read_buf()
.await
.map_err(HandshakeError::Io)?;
}
res = mid_stream.handshake();
}
},
}
}
}

View File

@ -0,0 +1,32 @@
use boring::ssl::{SslConnector, SslMethod};
use compio::io::AsyncReadExt;
use compio::net::TcpStream;
use compio_io::AsyncWrite;
use std::net::ToSocketAddrs;
#[compio::test]
async fn google() {
let addr = "google.com:443".to_socket_addrs().unwrap().next().unwrap();
let stream = TcpStream::connect(&addr).await.unwrap();
let config = SslConnector::builder(SslMethod::tls())
.unwrap()
.build()
.configure()
.unwrap();
let mut stream = compio_boring2::connect(config, "google.com", stream)
.await
.unwrap();
stream.write(b"GET / HTTP/1.0\r\n\r\n").await.unwrap();
stream.flush().await.unwrap();
let (_, buf) = stream.read_to_end(vec![]).await.unwrap();
stream.shutdown().await.unwrap();
let response = String::from_utf8_lossy(&buf);
let response = response.trim_end();
// any response code is fine
assert!(response.starts_with("HTTP/1.0 "));
assert!(response.ends_with("</html>") || response.ends_with("</HTML>"));
}