From 95704ba5c222f6153e293c5925c5385029eb441e Mon Sep 17 00:00:00 2001 From: Nathan Mittler Date: Thu, 23 Feb 2023 04:41:16 -0800 Subject: [PATCH] Introducing the Boring crypto provider. Also adding examples and basic documentation. --- .github/workflows/rust.yml | 133 +++++--- Cargo.toml | 35 ++- README.md | 34 +- deny.toml | 10 + examples/README.md | 45 +++ examples/client.rs | 152 +++++++++ examples/server.rs | 274 ++++++++++++++++ src/aead.rs | 152 +++++++++ src/alert.rs | 83 +++++ src/alpn.rs | 73 +++++ src/bffi_ext.rs | 565 +++++++++++++++++++++++++++++++++ src/client.rs | 421 +++++++++++++++++++++++++ src/error.rs | 150 +++++++++ src/handshake_token.rs | 68 ++++ src/hkdf.rs | 129 ++++++++ src/hmac.rs | 77 +++++ src/key.rs | 525 +++++++++++++++++++++++++++++++ src/key_log.rs | 90 ++++++ src/lib.rs | 296 ++++++++++++++++++ src/macros.rs | 120 +++++++ src/retry.rs | 98 ++++++ src/secret.rs | 207 +++++++++++++ src/server.rs | 329 ++++++++++++++++++++ src/session_cache.rs | 180 +++++++++++ src/session_state.rs | 621 +++++++++++++++++++++++++++++++++++++ src/suite.rs | 105 +++++++ src/version.rs | 151 +++++++++ tests/integration_tests.rs | 616 ++++++++++++++++++++++++++++++++++++ 28 files changed, 5689 insertions(+), 50 deletions(-) create mode 100644 deny.toml create mode 100644 examples/README.md create mode 100644 examples/client.rs create mode 100644 examples/server.rs create mode 100644 src/aead.rs create mode 100644 src/alert.rs create mode 100644 src/alpn.rs create mode 100644 src/bffi_ext.rs create mode 100644 src/client.rs create mode 100644 src/error.rs create mode 100644 src/handshake_token.rs create mode 100644 src/hkdf.rs create mode 100644 src/hmac.rs create mode 100644 src/key.rs create mode 100644 src/key_log.rs create mode 100644 src/macros.rs create mode 100644 src/retry.rs create mode 100644 src/secret.rs create mode 100644 src/server.rs create mode 100644 src/session_cache.rs create mode 100644 src/session_state.rs create mode 100644 src/suite.rs create mode 100644 src/version.rs create mode 100644 tests/integration_tests.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0b7cf49..0945ac2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,88 +8,125 @@ on: - cron: "21 3 * * 5" jobs: - test-freebsd: - # see https://github.com/actions/runner/issues/385 - # use https://github.com/vmactions/freebsd-vm for now - name: test on freebsd - runs-on: macos-12 - steps: - - uses: actions/checkout@v2 - - name: test on freebsd - uses: vmactions/freebsd-vm@v0 - with: - usesh: true - mem: 4096 - copyback: false - prepare: | - pkg install -y curl - curl https://sh.rustup.rs -sSf --output rustup.sh - sh rustup.sh -y --profile minimal --default-toolchain stable - echo "~~~~ rustc --version ~~~~" - $HOME/.cargo/bin/rustc --version - run: | - freebsd-version - $HOME/.cargo/bin/cargo build --all-targets - $HOME/.cargo/bin/cargo test + +# TODO(nmittler): Investigate why tests get "unknown CA" on windows. +# test-windows: +# name: test (windows-latest, stable) +# runs-on: windows-latest +# +# steps: +# - name: Checkout source +# uses: actions/checkout@v2 +# with: +# submodules: 'recursive' +# - name: Install nasm +# uses: crazy-max/ghaction-chocolatey@v1 +# with: +# args: install nasm +# - name: Install rust toolchain +# uses: actions-rs/toolchain@v1 +# with: +# profile: minimal +# toolchain: stable +# override: true +# - name: Cargo Build +# uses: actions-rs/cargo@v1 +# with: +# command: build +# args: --all-targets +# - name: Cargo Test +# uses: actions-rs/cargo@v1 +# with: +# command: test +# args: --verbose --all-targets + test: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - rust: [stable, beta, 1.59.0] + os: [ubuntu-latest, macos-latest] + rust: [stable, beta] exclude: - os: macos-latest rust: beta - - os: macos-latest - rust: 1.59.0 - - os: windows-latest - rust: beta - - os: windows-latest - rust: 1.59.0 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - name: Checkout source + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true - - uses: Swatinem/rust-cache@v1 - - uses: actions-rs/cargo@v1 + - name: Cargo Build + uses: actions-rs/cargo@v1 with: command: build args: --all-targets - - uses: actions-rs/cargo@v1 + - name: Cargo Test + uses: actions-rs/cargo@v1 with: command: test +# TODO(nmittler): Investigate build issues. +# test-fips: +# name: test fips +# runs-on: ubuntu-20.04 +# steps: +# - name: Checkout source +# uses: actions/checkout@v2 +# with: +# submodules: 'recursive' +# - name: Install Clang 7 +# uses: egor-tensin/setup-clang@v1 +# with: +# version: "7" +# - name: Install rust toolchain +# uses: actions-rs/toolchain@v1 +# with: +# profile: minimal +# toolchain: stable +# override: true +# - name: Cargo Build +# uses: actions-rs/cargo@v1 +# with: +# command: build +# args: --all-targets --features fips +# - name: Cargo Test +# uses: actions-rs/cargo@v1 +# with: +# command: test +# args: --features fips + lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - name: Checkout source + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true components: rustfmt, clippy - - uses: Swatinem/rust-cache@v1 - - uses: actions-rs/cargo@v1 + - name: Cargo fmt + uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - - uses: actions-rs/cargo@v1 + - name: Cargo clippy + uses: actions-rs/cargo@v1 with: command: clippy args: --all-targets -- -D warnings - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: clippy - - name: doc + - name: Cargo doc run: cargo doc --no-deps --document-private-items env: RUSTDOCFLAGS: -Dwarnings diff --git a/Cargo.toml b/Cargo.toml index 8afe8b1..1649b0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,42 @@ description = "BoringSSL crypto provider for quinn" keywords = ["quic"] categories = ["network-programming", "asynchronous"] edition = "2021" -rust-version = "1.59" [badges] maintenance = { status = "passively-maintained" } +[features] +fips = ["boring/fips", "boring-sys/fips"] + [dependencies] +boring = "2.1.0" +boring-sys = "2.1.0" +bytes = "1" +foreign-types-shared = "0.3.1" +lru = "0.9.0" +once_cell = "1.17" +quinn = { version = "0.9.3", default_features = false, features = ["native-certs", "runtime-tokio"] } +quinn-proto = { version = "0.9.3", default-features = false } +rand = "0.8" +tracing = "0.1" + +[dev-dependencies] +anyhow = "1.0.22" +assert_hex = "0.2.2" +assert_matches = "1.1" +clap = { version = "3.2", features = ["derive"] } +directories-next = "2" +hex-literal = "0.3.0" +ring = "0.16.7" +rcgen = "0.10.0" +rustls-pemfile = "1.0.0" +tokio = { version = "1.0.1", features = ["rt", "rt-multi-thread", "time", "macros", "sync"] } +tracing-futures = { version = "0.2.0", default-features = false, features = ["std-future"] } +tracing-subscriber = { version = "0.3.0", default-features = false, features = ["env-filter", "fmt", "ansi", "time", "local-time"] } +url = "2" + +[[example]] +name = "server" + +[[example]] +name = "client" diff --git a/README.md b/README.md index f87f5c1..a416450 100644 --- a/README.md +++ b/README.md @@ -1 +1,33 @@ -# TODO \ No newline at end of file +[![codecov](https://codecov.io/gh/quinn-rs/quinn/branch/main/graph/badge.svg)](https://codecov.io/gh/quinn-rs/quinn-boring) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE-APACHE) + +A crypto provider for [quinn](https://github.com/quinn-rs/quinn) based on [BoringSSL](https://github.com/google/boringssl). + +## Getting Started + +The [examples](examples) directory provides example client and server applications, which can be run as follows: + +```sh +$ cargo run --example server ./ +$ cargo run --example client https://localhost:4433/Cargo.toml +``` + +This launches an HTTP 0.9 server on the loopback address serving the current +working directory, with the client fetching `./Cargo.toml`. By default, the +server generates a self-signed certificate and stores it to disk, where the +client will automatically find and trust it. + +## Testing + +This repository relies on the [quinn_proto integration tests](https://github.com/quinn-rs/quinn/tree/main/quinn-proto/src/tests), +which can be made to run with the BoringSSL provider. + +## FIPS + +The BoringSSL provider is based on the Cloudflare [Boring library](https://github.com/cloudflare/boring), which +supports building against a FIPS-validated version of BoringSSL. + +## Authors + +* [Nathan Mittler](https://github.com/nmittler) - *Project owner* diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..c5d2843 --- /dev/null +++ b/deny.toml @@ -0,0 +1,10 @@ +[licenses] +allow-osi-fsf-free = "either" +copyleft = "warn" +exceptions = [{ allow = ["ISC", "MIT", "OpenSSL"], name = "ring" }] +private = { ignore = true } + +[[licenses.clarify]] +name = "ring" +expression = "ISC AND MIT AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..cfdb7ca --- /dev/null +++ b/examples/README.md @@ -0,0 +1,45 @@ +## HTTP/0.9 File Serving Example + +The examples in this directory were copied from [quinn](https://github.com/quinn-rs/quinn/tree/main/quinn/examples) +and modified to use BoringSSL. + +The `server` and `client` examples demonstrate fetching files using a HTTP-like toy protocol. + +1. Server (`server.rs`) + +The server listens for any client requesting a file. +If the file path is valid and allowed, it returns the contents. + +Open up a terminal and execute: + +```text +$ cargo run --example server ./ +``` + +2. Client (`client.rs`) + +The client requests a file and prints it to the console. +If the file is on the server, it will receive the response. + +In a new terminal execute: + +```test +$ cargo run --example client https://localhost:4433/Cargo.toml +``` + +where `Cargo.toml` is any file in the directory passed to the server. + +**Result:** + +The output will be the contents of this README. + +**Troubleshooting:** + +If the client times out with no activity on the server, try forcing the server to run on IPv4 by +running it with `cargo run --example server -- ./ --listen 127.0.0.1:4433`. The server listens on +IPv6 by default, `localhost` tends to resolve to IPv4, and support for accepting IPv4 packets on +IPv6 sockets varies between platforms. + +If the client prints `failed to process request: failed reading file`, the request was processed +successfully but the path segment of the URL did not correspond to a file in the directory being +served. diff --git a/examples/client.rs b/examples/client.rs new file mode 100644 index 0000000..9be021a --- /dev/null +++ b/examples/client.rs @@ -0,0 +1,152 @@ +//! This example demonstrates an HTTP client that requests files from a server. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + fs, + io::{self, Write}, + net::ToSocketAddrs, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::{anyhow, Result}; +use boring::x509::X509; +use clap::Parser; +use quinn_boring::QuicSslContext; +use tracing::{error, info}; +use url::Url; + +/// HTTP/0.9 over QUIC client +#[derive(Parser, Debug)] +#[clap(name = "client")] +struct Opt { + url: Url, + + /// Override hostname used for certificate verification + #[clap(long = "host")] + host: Option, + + /// Custom certificate authority to trust, in DER format + #[clap(parse(from_os_str), long = "ca")] + ca: Option, + + /// Simulate NAT rebinding after connecting + #[clap(long = "rebind")] + rebind: bool, +} + +fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + let opt = Opt::parse(); + let code = { + if let Err(e) = run(opt) { + eprintln!("ERROR: {}", e); + 1 + } else { + 0 + } + }; + std::process::exit(code); +} + +#[tokio::main] +async fn run(options: Opt) -> Result<()> { + let url = options.url; + let remote = (url.host_str().unwrap(), url.port().unwrap_or(4433)) + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow!("couldn't resolve to an address"))?; + + let mut client_crypto = quinn_boring::ClientConfig::new()?; + if let Some(ca_path) = options.ca { + client_crypto + .ctx_mut() + .cert_store_mut() + .add_cert(X509::from_der(&fs::read(ca_path)?)?)?; + } else { + let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap(); + let path = dirs.data_local_dir(); + info!("using cert directory: {:?}", path); + match fs::read(path.join("cert.der")) { + Ok(cert) => { + client_crypto + .ctx_mut() + .cert_store_mut() + .add_cert(X509::from_der(&cert)?)?; + } + Err(ref e) if e.kind() == io::ErrorKind::NotFound => { + info!("local server certificate not found"); + } + Err(e) => { + error!("failed to open local server certificate: {}", e); + } + } + } + + let mut endpoint = quinn_boring::helpers::client_endpoint("[::]:0".parse().unwrap())?; + endpoint.set_default_client_config(quinn::ClientConfig::new(Arc::new(client_crypto))); + + let request = format!("GET {}\r\n", url.path()); + let start = Instant::now(); + let rebind = options.rebind; + let host = options + .host + .as_ref() + .map_or_else(|| url.host_str(), |x| Some(x)) + .ok_or_else(|| anyhow!("no hostname specified"))?; + + eprintln!("connecting to {} at {}", host, remote); + let conn = endpoint + .connect(remote, host)? + .await + .map_err(|e| anyhow!("failed to connect: {}", e))?; + eprintln!("connected at {:?}", start.elapsed()); + let (mut send, recv) = conn + .open_bi() + .await + .map_err(|e| anyhow!("failed to open stream: {}", e))?; + if rebind { + let socket = std::net::UdpSocket::bind("[::]:0").unwrap(); + let addr = socket.local_addr().unwrap(); + eprintln!("rebinding to {}", addr); + endpoint.rebind(socket).expect("rebind failed"); + } + + send.write_all(request.as_bytes()) + .await + .map_err(|e| anyhow!("failed to send request: {}", e))?; + send.finish() + .await + .map_err(|e| anyhow!("failed to shutdown stream: {}", e))?; + let response_start = Instant::now(); + eprintln!("request sent at {:?}", response_start - start); + let resp = recv + .read_to_end(usize::MAX) + .await + .map_err(|e| anyhow!("failed to read response: {}", e))?; + let duration = response_start.elapsed(); + eprintln!( + "response received in {:?} - {} KiB/s", + duration, + resp.len() as f32 / (duration_secs(&duration) * 1024.0) + ); + io::stdout().write_all(&resp).unwrap(); + io::stdout().flush().unwrap(); + conn.close(0u32.into(), b"done"); + + // Give the server a fair chance to receive the close packet + endpoint.wait_idle().await; + + Ok(()) +} + +fn duration_secs(x: &Duration) -> f32 { + x.as_secs() as f32 + x.subsec_nanos() as f32 * 1e-9 +} diff --git a/examples/server.rs b/examples/server.rs new file mode 100644 index 0000000..6a46032 --- /dev/null +++ b/examples/server.rs @@ -0,0 +1,274 @@ +//! This example demonstrates an HTTP server that serves files from a directory. +//! +//! Checkout the `README.md` for guidance. + +use std::{ + ascii, fs, io, + net::SocketAddr, + path::{self, Path, PathBuf}, + str, + sync::Arc, +}; + +use anyhow::{anyhow, bail, Context, Result}; +use boring::pkey::PKey; +use boring::x509::X509; +use clap::Parser; +use quinn_boring::QuicSslContext; +use tracing::{error, info, info_span}; +use tracing_futures::Instrument as _; + +#[derive(Parser, Debug)] +#[clap(name = "server")] +struct Opt { + /// directory to serve files from + #[clap(parse(from_os_str))] + root: PathBuf, + /// TLS private key in PEM format + #[clap(parse(from_os_str), short = 'k', long = "key", requires = "cert")] + key: Option, + /// TLS certificate in PEM format + #[clap(parse(from_os_str), short = 'c', long = "cert", requires = "key")] + cert: Option, + /// Enable stateless retries + #[clap(long = "stateless-retry")] + stateless_retry: bool, + /// Address to listen on + #[clap(long = "listen", default_value = "[::1]:4433")] + listen: SocketAddr, +} + +fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); + let opt = Opt::parse(); + let code = { + if let Err(e) = run(opt) { + eprintln!("ERROR: {}", e); + 1 + } else { + 0 + } + }; + std::process::exit(code); +} + +#[tokio::main] +async fn run(options: Opt) -> Result<()> { + let (certs, key) = if let (Some(key_path), Some(cert_path)) = (&options.key, &options.cert) { + let key = fs::read(key_path).context("failed to read private key")?; + let key = if key_path.extension().map_or(false, |x| x == "der") { + PKey::private_key_from_der(&key)? + } else { + let pkcs8 = rustls_pemfile::pkcs8_private_keys(&mut &*key) + .context("malformed PKCS #8 private key")?; + match pkcs8.into_iter().next() { + Some(x) => PKey::private_key_from_der(&x)?, + None => { + let rsa = rustls_pemfile::rsa_private_keys(&mut &*key) + .context("malformed PKCS #1 private key")?; + match rsa.into_iter().next() { + Some(x) => PKey::private_key_from_der(&x)?, + None => { + bail!("no private keys found"); + } + } + } + } + }; + let cert_chain = fs::read(cert_path).context("failed to read certificate chain")?; + let cert_chain = if cert_path.extension().map_or(false, |x| x == "der") { + vec![X509::from_der(&cert_chain)?] + } else { + rustls_pemfile::certs(&mut &*cert_chain) + .context("invalid PEM-encoded certificate")? + .into_iter() + .map(|x| X509::from_der(&x).unwrap()) + .collect() + }; + + (cert_chain, key) + } else { + let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap(); + let path = dirs.data_local_dir(); + info!("using cert directory: {:?}", path); + let cert_path = path.join("cert.der"); + let key_path = path.join("key.der"); + + let (cert, key) = match fs::read(&cert_path).and_then(|x| Ok((x, fs::read(&key_path)?))) { + Ok(x) => x, + Err(ref e) if e.kind() == io::ErrorKind::NotFound => { + info!("generating self-signed certificate"); + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = cert.serialize_private_key_der(); + let cert = cert.serialize_der().unwrap(); + fs::create_dir_all(path).context("failed to create certificate directory")?; + fs::write(&cert_path, &cert).context("failed to write certificate")?; + fs::write(&key_path, &key).context("failed to write private key")?; + (cert, key) + } + Err(e) => { + bail!("failed to read certificate: {}", e); + } + }; + + let key = PKey::private_key_from_der(&key)?; + let cert = X509::from_der(&cert)?; + (vec![cert], key) + }; + + let mut server_crypto = quinn_boring::ServerConfig::new()?; + let ctx = server_crypto.ctx_mut(); + for (i, cert) in certs.iter().enumerate() { + if i == 0 { + ctx.set_certificate(cert.clone())?; + } else { + if i < certs.len() - 1 { + ctx.add_to_cert_chain(cert.clone())?; + } + ctx.cert_store_mut().add_cert(cert.clone())?; + } + } + ctx.set_private_key(key)?; + ctx.check_private_key()?; + + let mut server_config = quinn_boring::helpers::server_config(Arc::new(server_crypto))?; + Arc::get_mut(&mut server_config.transport) + .unwrap() + .max_concurrent_uni_streams(0_u8.into()); + if options.stateless_retry { + server_config.use_retry(true); + } + + let root = Arc::::from(options.root.clone()); + if !root.exists() { + bail!("root path does not exist"); + } + + let endpoint = quinn_boring::helpers::server_endpoint(server_config, options.listen)?; + eprintln!("listening on {}", endpoint.local_addr()?); + + while let Some(conn) = endpoint.accept().await { + info!("connection incoming"); + let fut = handle_connection(root.clone(), conn); + tokio::spawn(async move { + if let Err(e) = fut.await { + error!("connection failed: {reason}", reason = e.to_string()) + } + }); + } + + Ok(()) +} + +async fn handle_connection(root: Arc, conn: quinn::Connecting) -> Result<()> { + let connection = conn.await?; + let span = info_span!( + "connection", + remote = %connection.remote_address(), + protocol = %connection + .handshake_data() + .unwrap() + .downcast::().unwrap() + .protocol + .map_or_else(|| "".into(), |x| String::from_utf8_lossy(&x).into_owned()) + ); + async { + info!("established"); + + // Each stream initiated by the client constitutes a new request. + loop { + let stream = connection.accept_bi().await; + let stream = match stream { + Err(quinn::ConnectionError::ApplicationClosed { .. }) => { + info!("connection closed"); + return Ok(()); + } + Err(e) => { + return Err(e); + } + Ok(s) => s, + }; + let fut = handle_request(root.clone(), stream); + tokio::spawn( + async move { + if let Err(e) = fut.await { + error!("failed: {reason}", reason = e.to_string()); + } + } + .instrument(info_span!("request")), + ); + } + } + .instrument(span) + .await?; + Ok(()) +} + +async fn handle_request( + root: Arc, + (mut send, recv): (quinn::SendStream, quinn::RecvStream), +) -> Result<()> { + let req = recv + .read_to_end(64 * 1024) + .await + .map_err(|e| anyhow!("failed reading request: {}", e))?; + let mut escaped = String::new(); + for &x in &req[..] { + let part = ascii::escape_default(x).collect::>(); + escaped.push_str(str::from_utf8(&part).unwrap()); + } + info!(content = %escaped); + // Execute the request + let resp = process_get(&root, &req).unwrap_or_else(|e| { + error!("failed: {}", e); + format!("failed to process request: {}\n", e).into_bytes() + }); + // Write the response + send.write_all(&resp) + .await + .map_err(|e| anyhow!("failed to send response: {}", e))?; + // Gracefully terminate the stream + send.finish() + .await + .map_err(|e| anyhow!("failed to shutdown stream: {}", e))?; + info!("complete"); + Ok(()) +} + +fn process_get(root: &Path, x: &[u8]) -> Result> { + if x.len() < 4 || &x[0..4] != b"GET " { + bail!("missing GET"); + } + if x[4..].len() < 2 || &x[x.len() - 2..] != b"\r\n" { + bail!("missing \\r\\n"); + } + let x = &x[4..x.len() - 2]; + let end = x.iter().position(|&c| c == b' ').unwrap_or(x.len()); + let path = str::from_utf8(&x[..end]).context("path is malformed UTF-8")?; + let path = Path::new(&path); + let mut real_path = PathBuf::from(root); + let mut components = path.components(); + match components.next() { + Some(path::Component::RootDir) => {} + _ => { + bail!("path must be absolute"); + } + } + for c in components { + match c { + path::Component::Normal(x) => { + real_path.push(x); + } + x => { + bail!("illegal component in path: {:?}", x); + } + } + } + let data = fs::read(&real_path).context("failed reading file")?; + Ok(data) +} diff --git a/src/aead.rs b/src/aead.rs new file mode 100644 index 0000000..d535619 --- /dev/null +++ b/src/aead.rs @@ -0,0 +1,152 @@ +use crate::error::{map_result, Error, Result}; +use crate::key::{Key, Nonce, Tag}; +use boring_sys as bffi; +use once_cell::sync::Lazy; +use std::mem::MaybeUninit; + +const AES_128_GCM_KEY_LEN: usize = 16; +const AES_256_GCM_KEY_LEN: usize = 32; +const CHACHA20_POLY1305_KEY_LEN: usize = 32; + +const AES_GCM_NONCE_LEN: usize = 12; +const POLY1305_NONCE_LEN: usize = 12; + +pub(crate) const AES_GCM_TAG_LEN: usize = 16; +const POLY1305_TAG_LEN: usize = 16; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum ID { + Aes128Gcm, + Aes256Gcm, + Chacha20Poly1305, +} + +/// Wrapper around a raw BoringSSL EVP_AEAD. +#[derive(Copy, Clone, PartialEq, Eq)] +struct AeadPtr(*const bffi::EVP_AEAD); + +unsafe impl Send for AeadPtr {} +unsafe impl Sync for AeadPtr {} + +impl AeadPtr { + fn aes128_gcm() -> Self { + unsafe { Self(bffi::EVP_aead_aes_128_gcm()) } + } + + fn aes256_gcm() -> Self { + unsafe { Self(bffi::EVP_aead_aes_256_gcm()) } + } + + fn chacha20_poly1305() -> Self { + unsafe { Self(bffi::EVP_aead_chacha20_poly1305()) } + } +} + +/// Wrapper around an BoringSSL EVP_AEAD. +pub(crate) struct Aead { + ptr: AeadPtr, + pub(crate) id: ID, + pub(crate) key_len: usize, + pub(crate) tag_len: usize, + pub(crate) nonce_len: usize, +} + +impl PartialEq for Aead { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Aead {} + +static AES128_GCM: Lazy = Lazy::new(|| Aead { + ptr: AeadPtr::aes128_gcm(), + id: ID::Aes128Gcm, + key_len: AES_128_GCM_KEY_LEN, + tag_len: AES_GCM_TAG_LEN, + nonce_len: AES_GCM_NONCE_LEN, +}); + +static AES256_GCM: Lazy = Lazy::new(|| Aead { + ptr: AeadPtr::aes256_gcm(), + id: ID::Aes256Gcm, + key_len: AES_256_GCM_KEY_LEN, + tag_len: AES_GCM_TAG_LEN, + nonce_len: AES_GCM_NONCE_LEN, +}); + +static CHACHA20_POLY1305: Lazy = Lazy::new(|| Aead { + ptr: AeadPtr::chacha20_poly1305(), + id: ID::Chacha20Poly1305, + key_len: CHACHA20_POLY1305_KEY_LEN, + tag_len: POLY1305_TAG_LEN, + nonce_len: POLY1305_NONCE_LEN, +}); + +impl Aead { + #[inline] + pub(crate) fn aes128_gcm() -> &'static Self { + &AES128_GCM + } + + #[inline] + pub(crate) fn aes256_gcm() -> &'static Self { + &AES256_GCM + } + + #[inline] + pub(crate) fn chacha20_poly1305() -> &'static Self { + &CHACHA20_POLY1305 + } + + /// Creates a new zeroed key of the appropriate length for the AEAD algorithm. + #[inline] + pub(crate) fn zero_key(&self) -> Key { + Key::with_len(self.key_len) + } + + /// Creates a new zeroed nonce of the appropriate length for the AEAD algorithm. + #[inline] + pub(crate) fn zero_nonce(&self) -> Nonce { + Nonce::with_len(self.nonce_len) + } + + /// Creates a new zeroed tag of the appropriate length for the AEAD algorithm. + #[inline] + pub(crate) fn zero_tag(&self) -> Tag { + Tag::with_len(self.tag_len) + } + + #[inline] + pub(crate) fn as_ptr(&self) -> *const bffi::EVP_AEAD { + self.ptr.0 + } + + #[inline] + pub(crate) fn new_aead_ctx(&self, key: &Key) -> Result { + if key.len() != self.key_len { + return Err(Error::invalid_input(format!( + "key length invalid for AEAD_CTX: {}", + key.len() + ))); + } + + let ctx = unsafe { + let mut ctx = MaybeUninit::uninit(); + + map_result(bffi::EVP_AEAD_CTX_init( + ctx.as_mut_ptr(), + self.as_ptr(), + key.as_ptr(), + key.len(), + self.tag_len, + std::ptr::null_mut(), + ))?; + + ctx.assume_init() + }; + + Ok(ctx) + } +} diff --git a/src/alert.rs b/src/alert.rs new file mode 100644 index 0000000..ac81425 --- /dev/null +++ b/src/alert.rs @@ -0,0 +1,83 @@ +use boring_sys as bffi; +use quinn_proto::{TransportError, TransportErrorCode}; +use std::ffi::{c_int, CStr}; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +pub(crate) enum AlertType { + Warning, + Fatal, + Unknown, +} + +impl AlertType { + const ALERT_TYPE_WARNING: &'static str = "warning"; + const ALERT_TYPE_FATAL: &'static str = "fatal"; + const ALERT_TYPE_UNKNOWN: &'static str = "unknown"; +} + +impl Display for AlertType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Warning => f.write_str(Self::ALERT_TYPE_WARNING), + Self::Fatal => f.write_str(Self::ALERT_TYPE_FATAL), + _ => f.write_str(Self::ALERT_TYPE_UNKNOWN), + } + } +} + +impl FromStr for AlertType { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + Self::ALERT_TYPE_WARNING => Ok(Self::Warning), + Self::ALERT_TYPE_FATAL => Ok(Self::Fatal), + _ => Ok(Self::Unknown), + } + } +} + +#[derive(Copy, Clone)] +pub(crate) struct Alert(u8); + +impl Alert { + pub(crate) fn from(value: u8) -> Self { + Alert(value) + } + + pub(crate) fn handshake_failure() -> Self { + Alert(bffi::SSL_AD_HANDSHAKE_FAILURE as u8) + } + + pub(crate) fn get_description(&self) -> &'static str { + unsafe { + CStr::from_ptr(bffi::SSL_alert_desc_string_long(self.0 as c_int)) + .to_str() + .unwrap() + } + } +} + +impl Display for Alert { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "SSL alert [{}]: {}", self.0, self.get_description()) + } +} + +impl From for TransportErrorCode { + fn from(alert: Alert) -> Self { + TransportErrorCode::crypto(alert.0) + } +} + +impl From for TransportError { + fn from(alert: Alert) -> Self { + TransportError { + code: alert.into(), + frame: None, + reason: alert.get_description().to_string(), + } + } +} diff --git a/src/alpn.rs b/src/alpn.rs new file mode 100644 index 0000000..4e094ae --- /dev/null +++ b/src/alpn.rs @@ -0,0 +1,73 @@ +use crate::error::{Error, Result}; + +#[derive(Clone, Debug)] +pub(crate) struct AlpnProtocol(Vec); + +impl AlpnProtocol { + #[inline] + pub(crate) fn encode(&self, encoded: &mut Vec) { + encoded.push(self.0.len() as u8); + encoded.extend_from_slice(&self.0); + } +} + +impl From> for AlpnProtocol { + fn from(value: Vec) -> Self { + Self(value) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct AlpnProtocols(Vec); + +impl AlpnProtocols { + pub(crate) const H3: &'static [u8; 2] = b"h3"; + + /// Performs the server-side ALPN protocol selection. + pub(crate) fn select<'a>(&self, offered: &'a [u8]) -> Result<&'a [u8]> { + for server_proto in &self.0 { + let mut i = 0; + while i < offered.len() { + let len = offered[i] as usize; + i += 1; + + let client_proto = &offered[i..i + len]; + if server_proto.0 == client_proto { + return Ok(client_proto); + } + i += len; + } + } + Err(Error::other("ALPN selection failed".into())) + } + + pub(crate) fn encode(&self) -> Vec { + let mut out: Vec = Vec::new(); + for proto in &self.0 { + proto.encode(&mut out); + } + out + } +} + +impl Default for AlpnProtocols { + fn default() -> Self { + Self::from(&[Self::H3.to_vec()][..]) + } +} + +impl From<&[Vec]> for AlpnProtocols { + fn from(protos: &[Vec]) -> Self { + let mut out = Vec::with_capacity(protos.len()); + for proto in protos { + out.push(AlpnProtocol(proto.clone())) + } + Self(out) + } +} + +impl From<&Vec>> for AlpnProtocols { + fn from(protos: &Vec>) -> Self { + Self::from(protos.as_slice()) + } +} diff --git a/src/bffi_ext.rs b/src/bffi_ext.rs new file mode 100644 index 0000000..3da5840 --- /dev/null +++ b/src/bffi_ext.rs @@ -0,0 +1,565 @@ +use crate::error::{br, br_zero_is_success, BoringResult}; +use boring::error::ErrorStack; +use boring::pkey::{HasPrivate, PKey}; +use boring::ssl::{Ssl, SslContext, SslContextRef, SslSession}; +use boring::x509::store::X509StoreBuilderRef; +use boring::x509::X509; +use boring_sys as bffi; +use bytes::{Buf, BufMut}; +use foreign_types_shared::{ForeignType, ForeignTypeRef}; +use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; +use std::fmt::{Display, Formatter}; +use std::result::Result as StdResult; +use std::{ffi, fmt, mem, ptr, slice}; + +/// Provides additional methods to [SslContext] needed for QUIC. +pub trait QuicSslContext { + fn set_options(&mut self, options: u32) -> u32; + fn verify_peer(&mut self, verify: bool); + fn set_quic_method(&mut self, method: &bffi::SSL_QUIC_METHOD) -> BoringResult; + fn set_session_cache_mode(&mut self, mode: c_int) -> c_int; + fn set_new_session_callback( + &mut self, + cb: Option< + unsafe extern "C" fn(ssl: *mut bffi::SSL, session: *mut bffi::SSL_SESSION) -> c_int, + >, + ); + fn set_info_callback( + &mut self, + cb: Option, + ); + fn set_keylog_callback( + &mut self, + cb: Option, + ); + fn set_certificate(&mut self, cert: X509) -> BoringResult; + fn load_certificate_from_pem_file(&mut self, path: &str) -> BoringResult; + fn add_to_cert_chain(&mut self, cert: X509) -> BoringResult; + fn load_cert_chain_from_pem_file(&mut self, path: &str) -> BoringResult; + fn set_private_key(&mut self, key: PKey) -> BoringResult; + fn load_private_key_from_pem_file(&mut self, path: &str) -> BoringResult; + fn check_private_key(&self) -> BoringResult; + fn cert_store_mut(&mut self) -> &mut X509StoreBuilderRef; + + fn enable_early_data(&mut self, enable: bool); + fn set_alpn_protos(&mut self, protos: &[u8]) -> BoringResult; + fn set_alpn_select_cb( + &mut self, + cb: Option< + unsafe extern "C" fn( + ssl: *mut bffi::SSL, + out: *mut *const u8, + out_len: *mut u8, + in_: *const u8, + in_len: c_uint, + arg: *mut c_void, + ) -> c_int, + >, + ); + fn set_server_name_cb( + &mut self, + cb: Option< + unsafe extern "C" fn( + ssl: *mut bffi::SSL, + out_alert: *mut c_int, + arg: *mut c_void, + ) -> c_int, + >, + ); + fn set_select_certificate_cb( + &mut self, + cb: Option< + unsafe extern "C" fn( + arg1: *const bffi::SSL_CLIENT_HELLO, + ) -> bffi::ssl_select_cert_result_t, + >, + ); +} + +impl QuicSslContext for SslContext { + fn set_options(&mut self, options: u32) -> u32 { + unsafe { bffi::SSL_CTX_set_options(self.as_ptr(), options) } + } + + fn verify_peer(&mut self, verify: bool) { + let mode = if verify { + bffi::SSL_VERIFY_PEER | bffi::SSL_VERIFY_FAIL_IF_NO_PEER_CERT + } else { + bffi::SSL_VERIFY_NONE + }; + + unsafe { bffi::SSL_CTX_set_verify(self.as_ptr(), mode, None) } + } + + fn set_quic_method(&mut self, method: &bffi::SSL_QUIC_METHOD) -> BoringResult { + unsafe { br(bffi::SSL_CTX_set_quic_method(self.as_ptr(), method)) } + } + + fn set_session_cache_mode(&mut self, mode: c_int) -> c_int { + unsafe { bffi::SSL_CTX_set_session_cache_mode(self.as_ptr(), mode) } + } + + fn set_new_session_callback( + &mut self, + cb: Option< + unsafe extern "C" fn(ssl: *mut bffi::SSL, session: *mut bffi::SSL_SESSION) -> c_int, + >, + ) { + unsafe { + bffi::SSL_CTX_sess_set_new_cb(self.as_ptr(), cb); + } + } + + fn set_info_callback( + &mut self, + cb: Option, + ) { + unsafe { bffi::SSL_CTX_set_info_callback(self.as_ptr(), cb) } + } + + fn set_keylog_callback( + &mut self, + cb: Option, + ) { + unsafe { bffi::SSL_CTX_set_keylog_callback(self.as_ptr(), cb) } + } + + fn set_certificate(&mut self, cert: X509) -> BoringResult { + unsafe { + br(bffi::SSL_CTX_use_certificate(self.as_ptr(), cert.as_ptr()))?; + mem::forget(cert); + Ok(()) + } + } + + fn load_certificate_from_pem_file(&mut self, path: &str) -> BoringResult { + let path = ffi::CString::new(path).unwrap(); + unsafe { + br(bffi::SSL_CTX_use_certificate_file( + self.as_ptr(), + path.as_ptr(), + bffi::SSL_FILETYPE_PEM, + )) + } + } + + fn add_to_cert_chain(&mut self, cert: X509) -> BoringResult { + unsafe { + br(bffi::SSL_CTX_add_extra_chain_cert(self.as_ptr(), cert.as_ptr()) as c_int)?; + mem::forget(cert); + Ok(()) + } + } + + fn load_cert_chain_from_pem_file(&mut self, path: &str) -> BoringResult { + let path = ffi::CString::new(path).unwrap(); + unsafe { + br(bffi::SSL_CTX_use_certificate_chain_file( + self.as_ptr(), + path.as_ptr(), + )) + } + } + + fn set_private_key(&mut self, key: PKey) -> BoringResult { + unsafe { + br(bffi::SSL_CTX_use_PrivateKey(self.as_ptr(), key.as_ptr()))?; + mem::forget(key); + Ok(()) + } + } + + fn load_private_key_from_pem_file(&mut self, path: &str) -> BoringResult { + let path = ffi::CString::new(path).unwrap(); + + unsafe { + br(bffi::SSL_CTX_use_PrivateKey_file( + self.as_ptr(), + path.as_ptr(), + bffi::SSL_FILETYPE_PEM, + )) + } + } + + fn check_private_key(&self) -> BoringResult { + unsafe { br(bffi::SSL_CTX_check_private_key(self.as_ptr())) } + } + + fn cert_store_mut(&mut self) -> &mut X509StoreBuilderRef { + unsafe { X509StoreBuilderRef::from_ptr_mut(bffi::SSL_CTX_get_cert_store(self.as_ptr())) } + } + + fn enable_early_data(&mut self, enable: bool) { + unsafe { bffi::SSL_CTX_set_early_data_enabled(self.as_ptr(), enable.into()) } + } + + fn set_alpn_protos(&mut self, protos: &[u8]) -> BoringResult { + unsafe { + br_zero_is_success(bffi::SSL_CTX_set_alpn_protos( + self.as_ptr(), + protos.as_ptr(), + protos.len() as _, + )) + } + } + + fn set_alpn_select_cb( + &mut self, + cb: Option< + unsafe extern "C" fn( + *mut bffi::SSL, + *mut *const u8, + *mut u8, + *const u8, + c_uint, + *mut c_void, + ) -> c_int, + >, + ) { + unsafe { bffi::SSL_CTX_set_alpn_select_cb(self.as_ptr(), cb, ptr::null_mut()) } + } + + fn set_server_name_cb( + &mut self, + cb: Option< + unsafe extern "C" fn( + ssl: *mut bffi::SSL, + out_alert: *mut c_int, + arg: *mut c_void, + ) -> c_int, + >, + ) { + // The function always returns 1. + unsafe { + let _ = bffi::SSL_CTX_set_tlsext_servername_callback(self.as_ptr(), cb); + } + } + + fn set_select_certificate_cb( + &mut self, + cb: Option< + unsafe extern "C" fn( + arg1: *const bffi::SSL_CLIENT_HELLO, + ) -> bffi::ssl_select_cert_result_t, + >, + ) { + unsafe { bffi::SSL_CTX_set_select_certificate_cb(self.as_ptr(), cb) } + } +} + +/// Provides additional methods to [Ssl] needed for QUIC. +pub trait QuicSsl { + fn set_connect_state(&mut self); + fn set_accept_state(&mut self); + fn state_string(&self) -> &'static str; + fn set_quic_transport_params(&mut self, params: &[u8]) -> BoringResult; + fn get_peer_quic_transport_params(&self) -> Option<&[u8]>; + fn get_error(&self, raw: c_int) -> SslError; + fn is_handshaking(&self) -> bool; + fn do_handshake(&mut self) -> SslError; + fn provide_quic_data(&mut self, level: Level, data: &[u8]) -> SslError; + fn quic_max_handshake_flight_len(&self, level: Level) -> usize; + fn quic_read_level(&self) -> Level; + fn quic_write_level(&self) -> Level; + fn process_post_handshake(&mut self) -> SslError; + fn set_verify_hostname(&mut self, domain: &str) -> BoringResult; + fn export_keyring_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> BoringResult; + + fn in_early_data(&self) -> bool; + fn early_data_accepted(&self) -> bool; + fn set_quic_method(&mut self, method: &bffi::SSL_QUIC_METHOD) -> BoringResult; + fn set_quic_early_data_context(&mut self, value: &[u8]) -> BoringResult; + fn get_early_data_reason(&self) -> bffi::ssl_early_data_reason_t; + fn early_data_reason_string(reason: bffi::ssl_early_data_reason_t) -> &'static str; + fn reset_early_rejected_data(&mut self); + fn set_quic_use_legacy_codepoint(&mut self, use_legacy: bool); +} + +impl QuicSsl for Ssl { + fn set_connect_state(&mut self) { + unsafe { bffi::SSL_set_connect_state(self.as_ptr()) } + } + + fn set_accept_state(&mut self) { + unsafe { bffi::SSL_set_accept_state(self.as_ptr()) } + } + + fn state_string(&self) -> &'static str { + unsafe { + CStr::from_ptr(bffi::SSL_state_string_long(self.as_ptr())) + .to_str() + .unwrap() + } + } + + fn set_quic_transport_params(&mut self, params: &[u8]) -> BoringResult { + unsafe { + br(bffi::SSL_set_quic_transport_params( + self.as_ptr(), + params.as_ptr(), + params.len(), + )) + } + } + + fn get_peer_quic_transport_params(&self) -> Option<&[u8]> { + let mut ptr: *const u8 = ptr::null(); + let mut len: usize = 0; + + unsafe { + bffi::SSL_get_peer_quic_transport_params(self.as_ptr(), &mut ptr, &mut len); + + if len == 0 { + None + } else { + Some(slice::from_raw_parts(ptr, len)) + } + } + } + + #[inline] + fn get_error(&self, raw: c_int) -> SslError { + unsafe { SslError(bffi::SSL_get_error(self.as_ptr(), raw)) } + } + + #[inline] + fn is_handshaking(&self) -> bool { + unsafe { bffi::SSL_in_init(self.as_ptr()) == 1 } + } + + #[inline] + fn do_handshake(&mut self) -> SslError { + self.get_error(unsafe { bffi::SSL_do_handshake(self.as_ptr()) }) + } + + #[inline] + fn provide_quic_data(&mut self, level: Level, plaintext: &[u8]) -> SslError { + unsafe { + self.get_error(bffi::SSL_provide_quic_data( + self.as_ptr(), + level.into(), + plaintext.as_ptr(), + plaintext.len(), + )) + } + } + + #[inline] + fn quic_max_handshake_flight_len(&self, level: Level) -> usize { + unsafe { bffi::SSL_quic_max_handshake_flight_len(self.as_ptr(), level.into()) } + } + + #[inline] + fn quic_read_level(&self) -> Level { + unsafe { bffi::SSL_quic_read_level(self.as_ptr()).into() } + } + + #[inline] + fn quic_write_level(&self) -> Level { + unsafe { bffi::SSL_quic_write_level(self.as_ptr()).into() } + } + + #[inline] + fn process_post_handshake(&mut self) -> SslError { + self.get_error(unsafe { bffi::SSL_process_quic_post_handshake(self.as_ptr()) }) + } + + fn set_verify_hostname(&mut self, domain: &str) -> BoringResult { + let param = self.param_mut(); + param.set_hostflags(boring::x509::verify::X509CheckFlags::NO_PARTIAL_WILDCARDS); + match domain.parse() { + Ok(ip) => param.set_ip(ip)?, + Err(_) => param.set_host(domain)?, + } + Ok(()) + } + + #[inline] + fn export_keyring_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> BoringResult { + unsafe { + br(bffi::SSL_export_keying_material( + self.as_ptr(), + output.as_mut_ptr(), + output.len(), + label.as_ptr() as *const c_char, + label.len(), + context.as_ptr(), + context.len(), + context.is_empty() as _, + )) + } + } + + #[inline] + fn in_early_data(&self) -> bool { + unsafe { bffi::SSL_in_early_data(self.as_ptr()) == 1 } + } + + #[inline] + fn early_data_accepted(&self) -> bool { + unsafe { bffi::SSL_early_data_accepted(self.as_ptr()) == 1 } + } + + fn set_quic_method(&mut self, method: &bffi::SSL_QUIC_METHOD) -> BoringResult { + unsafe { br(bffi::SSL_set_quic_method(self.as_ptr(), method)) } + } + + fn set_quic_early_data_context(&mut self, value: &[u8]) -> BoringResult { + unsafe { + br(bffi::SSL_set_quic_early_data_context( + self.as_ptr(), + value.as_ptr(), + value.len(), + )) + } + } + + fn get_early_data_reason(&self) -> bffi::ssl_early_data_reason_t { + unsafe { bffi::SSL_get_early_data_reason(self.as_ptr()) } + } + + fn early_data_reason_string(reason: bffi::ssl_early_data_reason_t) -> &'static str { + unsafe { + bffi::SSL_early_data_reason_string(reason) + .as_ref() + .map_or("unknown", |reason| CStr::from_ptr(reason).to_str().unwrap()) + } + } + + #[inline] + fn reset_early_rejected_data(&mut self) { + unsafe { bffi::SSL_reset_early_data_reject(self.as_ptr()) } + } + + fn set_quic_use_legacy_codepoint(&mut self, use_legacy: bool) { + unsafe { bffi::SSL_set_quic_use_legacy_codepoint(self.as_ptr(), use_legacy as _) } + } +} + +pub trait QuicSslSession { + fn early_data_capable(&self) -> bool; + fn copy_without_early_data(&mut self) -> SslSession; + fn encode(&self, out: &mut W) -> BoringResult; + fn decode(ctx: &SslContextRef, r: &mut R) -> StdResult; +} + +impl QuicSslSession for SslSession { + fn early_data_capable(&self) -> bool { + unsafe { bffi::SSL_SESSION_early_data_capable(self.as_ptr()) == 1 } + } + + fn copy_without_early_data(&mut self) -> SslSession { + unsafe { SslSession::from_ptr(bffi::SSL_SESSION_copy_without_early_data(self.as_ptr())) } + } + + fn encode(&self, out: &mut W) -> BoringResult { + unsafe { + let mut buf: *mut u8 = ptr::null_mut(); + let mut len = 0usize; + br(bffi::SSL_SESSION_to_bytes( + self.as_ptr(), + &mut buf, + &mut len, + ))?; + out.put_slice(slice::from_raw_parts(buf, len)); + bffi::OPENSSL_free(buf as _); + Ok(()) + } + } + + fn decode(ctx: &SslContextRef, r: &mut R) -> StdResult { + unsafe { + let in_len = r.remaining(); + let in_ = r.chunk(); + bffi::SSL_SESSION_from_bytes(in_.as_ptr(), in_len, ctx.as_ptr()) + .as_mut() + .map_or_else( + || Err(ErrorStack::get()), + |session| Ok(SslSession::from_ptr(session)), + ) + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum Level { + Initial = 0, + EarlyData = 1, + Handshake = 2, + Application = 3, +} + +impl Level { + pub const NUM_LEVELS: usize = 4; + + pub fn next(&self) -> Self { + match self { + Level::Initial => Level::Handshake, + Level::EarlyData => Level::Handshake, + _ => Level::Application, + } + } +} + +impl From for Level { + fn from(value: bffi::ssl_encryption_level_t) -> Self { + match value { + bffi::ssl_encryption_level_t::ssl_encryption_initial => Self::Initial, + bffi::ssl_encryption_level_t::ssl_encryption_early_data => Self::EarlyData, + bffi::ssl_encryption_level_t::ssl_encryption_handshake => Self::Handshake, + bffi::ssl_encryption_level_t::ssl_encryption_application => Self::Application, + _ => unreachable!(), + } + } +} + +impl From for bffi::ssl_encryption_level_t { + fn from(value: Level) -> Self { + match value { + Level::Initial => bffi::ssl_encryption_level_t::ssl_encryption_initial, + Level::EarlyData => bffi::ssl_encryption_level_t::ssl_encryption_early_data, + Level::Handshake => bffi::ssl_encryption_level_t::ssl_encryption_handshake, + Level::Application => bffi::ssl_encryption_level_t::ssl_encryption_application, + } + } +} + +#[derive(Copy, Clone)] +pub struct SslError(c_int); + +impl SslError { + #[inline] + pub fn value(&self) -> c_int { + self.0 + } + + #[inline] + pub fn is_none(&self) -> bool { + self.0 == bffi::SSL_ERROR_NONE + } + + #[inline] + pub fn get_description(&self) -> &'static str { + unsafe { + CStr::from_ptr(bffi::SSL_error_description(self.0)) + .to_str() + .unwrap() + } + } +} + +impl Display for SslError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "SSL_ERROR[{}]: {}", self.0, self.get_description()) + } +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..291910d --- /dev/null +++ b/src/client.rs @@ -0,0 +1,421 @@ +use crate::alpn::AlpnProtocols; +use crate::bffi_ext::QuicSslContext; +use crate::error::{map_result, Result}; +use crate::session_state::{SessionState, QUIC_METHOD}; +use crate::version::QuicVersion; +use crate::{Entry, KeyLog, NoKeyLog, QuicSsl, QuicSslSession, SessionCache, SimpleCache}; +use boring::ssl::{Ssl, SslContext, SslContextBuilder, SslMethod, SslSession, SslVersion}; +use boring_sys as bffi; +use bytes::{Bytes, BytesMut}; +use foreign_types_shared::ForeignType; +use once_cell::sync::Lazy; +use quinn_proto::{ + crypto, transport_parameters::TransportParameters, ConnectError, ConnectionId, Side, + TransportError, +}; +use std::any::Any; +use std::ffi::c_int; +use std::io::Cursor; +use std::result::Result as StdResult; +use std::sync::Arc; +use tracing::{trace, warn}; + +/// Configuration for a client-side QUIC. Wraps around a BoringSSL [SslContext]. +pub struct Config { + ctx: SslContext, + session_cache: Arc, + key_log: Option>, +} + +impl Config { + pub fn new() -> Result { + let mut builder = SslContextBuilder::new(SslMethod::tls())?; + + // QUIC requires TLS 1.3. + builder.set_min_proto_version(Some(SslVersion::TLS1_3))?; + builder.set_max_proto_version(Some(SslVersion::TLS1_3))?; + + builder.set_default_verify_paths()?; + + // We build the context early, since we are not allowed to further mutate the context + // in start_session. + let mut ctx = builder.build(); + + // By default, enable early data (used for 0-RTT). + ctx.enable_early_data(true); + + // Set the default ALPN protocols offered by the client. QUIC requires ALPN be configured + // (see ). + ctx.set_alpn_protos(&AlpnProtocols::default().encode())?; + + // Configure session caching. + ctx.set_session_cache_mode(bffi::SSL_SESS_CACHE_CLIENT | bffi::SSL_SESS_CACHE_NO_INTERNAL); + ctx.set_new_session_callback(Some(Session::new_session_callback)); + + // Set callbacks for the SessionState. + ctx.set_quic_method(&QUIC_METHOD)?; + ctx.set_info_callback(Some(SessionState::info_callback)); + + // For clients, verification of the server is on by default. + ctx.verify_peer(true); + + Ok(Self { + ctx, + session_cache: Arc::new(SimpleCache::new(256)), + key_log: None, + }) + } + + /// Returns the underlying [SslContext] backing all created sessions. + pub fn ctx(&self) -> &SslContext { + &self.ctx + } + + /// Returns the underlying [SslContext] backing all created sessions. Wherever possible use + /// the provided methods to modify settings rather than accessing this directly. + /// + /// Care should be taken to avoid overriding required behavior. In particular, this + /// configuration will set callbacks for QUIC events, alpn selection, server name, + /// as well as info and key logging. + pub fn ctx_mut(&mut self) -> &mut SslContext { + &mut self.ctx + } + + /// Sets whether or not the peer certificate should be verified. If `true`, any error + /// during verification will be fatal. If not called, verification of the server is + /// enabled by default. + pub fn verify_peer(&mut self, verify: bool) { + self.ctx.verify_peer(verify) + } + + /// Gets the [SessionCache] used to cache all client sessions. + pub fn get_session_cache(&self) -> Arc { + self.session_cache.clone() + } + + /// Sets the [SessionCache] to be shared by all created client sessions. + pub fn set_session_cache(&mut self, session_cache: Arc) { + self.session_cache = session_cache; + } + + /// Sets the ALPN protocols supported by the client. QUIC requires that + /// ALPN be used (see ). + /// By default, the client will offer "h3". + pub fn set_alpn(&mut self, alpn_protocols: &[Vec]) -> Result<()> { + self.ctx + .set_alpn_protos(&AlpnProtocols::from(alpn_protocols).encode())?; + Ok(()) + } + + /// Sets the [KeyLog] for the client. By default, no key logging will occur. + pub fn set_key_log(&mut self, key_log: Option>) { + self.key_log = key_log; + + // Optimization for key logging. Only set the callback if a logger was supplied, + // since the BoringSSL processing isn't free. + match &self.key_log { + Some(_) => { + self.ctx + .set_keylog_callback(Some(SessionState::keylog_callback)); + } + None => { + self.ctx.set_keylog_callback(None); + } + } + } +} + +impl crypto::ClientConfig for Config { + fn start_session( + self: Arc, + version: u32, + server_name: &str, + params: &TransportParameters, + ) -> StdResult, ConnectError> { + let version = QuicVersion::parse(version).unwrap(); + + Ok(Session::new(self, version, server_name, params) + .map_err(|_| ConnectError::EndpointStopping)?) + } +} + +static SESSION_INDEX: Lazy = Lazy::new(|| unsafe { + bffi::SSL_get_ex_new_index(0, std::ptr::null_mut(), std::ptr::null_mut(), None, None) +}); + +/// The [crypto::Session] implementation for BoringSSL. +struct Session { + state: Box, + server_name: Bytes, + session_cache: Arc, + zero_rtt_peer_params: Option, + handshake_data_available: bool, + handshake_data_sent: bool, +} + +impl Session { + fn new( + cfg: Arc, + version: QuicVersion, + server_name: &str, + params: &TransportParameters, + ) -> Result> { + let session_cache = cfg.session_cache.clone(); + let mut ssl = Ssl::new(&cfg.ctx).unwrap(); + + // Configure the TLS extension based on the QUIC version used. + ssl.set_quic_use_legacy_codepoint(version.uses_legacy_extension()); + + // Configure the SSL to be a client. + ssl.set_connect_state(); + + // Configure verification for the server hostname. + ssl.set_verify_hostname(server_name) + .map_err(|_| ConnectError::InvalidDnsName(server_name.into()))?; + + // Set the SNI hostname. + // TODO: should we validate the hostname? + ssl.set_hostname(server_name) + .map_err(|_| ConnectError::InvalidDnsName(server_name.into()))?; + + // Set the transport parameters. + ssl.set_quic_transport_params(&encode_params(params)) + .map_err(|_| ConnectError::EndpointStopping)?; + + let server_name_bytes = Bytes::copy_from_slice(server_name.as_bytes()); + + // If we have a cached session, use it. + let mut zero_rtt_peer_params = None; + if let Some(entry) = session_cache.get(server_name_bytes.clone()) { + match Entry::decode(ssl.ssl_context(), entry) { + Ok(entry) => { + zero_rtt_peer_params = Some(entry.params); + match unsafe { ssl.set_session(entry.session.as_ref()) } { + Ok(()) => { + trace!("attempting resumption (0-RTT) for server: {}.", server_name); + } + Err(e) => { + warn!( + "failed setting cached session for server {}: {:?}", + server_name, e + ) + } + } + } + Err(e) => { + warn!( + "failed decoding session entry for server {}: {:?}", + server_name, e + ) + } + } + } else { + trace!( + "no cached session found for server: {}. Will continue with 1-RTT.", + server_name + ); + } + + let mut session = Box::new(Self { + state: SessionState::new( + ssl, + Side::Client, + version, + cfg.key_log + .as_ref() + .map_or(Arc::new(NoKeyLog), |key_log| key_log.clone()), + )?, + server_name: server_name_bytes, + session_cache, + zero_rtt_peer_params, + handshake_data_available: false, + handshake_data_sent: false, + }); + + // Register the instance in SSL ex_data. This allows the static callbacks to + // reference the instance. + unsafe { + map_result(bffi::SSL_set_ex_data( + session.state.ssl.as_ptr(), + *SESSION_INDEX, + &mut *session as *mut Self as *mut _, + ))?; + } + + // Start the handshake in order to emit the Client Hello on the first + // call to write_handshake. + session.state.advance_handshake()?; + + Ok(session) + } + + /// Handler for the rejection of a 0-RTT attempt. Will continue with 1-RTT. + fn on_zero_rtt_rejected(&mut self) { + trace!( + "0-RTT handshake attempted but was rejected by the server: {}", + Ssl::early_data_reason_string(self.state.ssl.get_early_data_reason()) + ); + + self.zero_rtt_peer_params = None; + + // Removed the failed cache entry. + self.session_cache.remove(self.server_name.clone()); + + // Now retry advancing the handshake, this time in 1-RTT mode. + if let Err(e) = self.state.advance_handshake() { + warn!("failed advancing 1-RTT handshake: {:?}", e) + } + } + + /// Client-side only callback from BoringSSL to allow caching of a new session. + fn on_new_session(&mut self, session: SslSession) { + if !session.early_data_capable() { + warn!("failed caching session: not early data capable"); + return; + } + + // Get the server transport parameters. + let params = match self.state.ssl.get_peer_quic_transport_params() { + Some(params) => { + match TransportParameters::read(Side::Client, &mut Cursor::new(¶ms)) { + Ok(params) => params, + Err(e) => { + warn!("failed parsing server transport parameters: {:?}", e); + return; + } + } + } + None => { + warn!("failed caching session: server transport parameters are not available"); + return; + } + }; + + // Encode the session cache entry, including both the session and the server params. + let entry = Entry { session, params }; + match entry.encode() { + Ok(value) => { + // Cache the session. + self.session_cache.put(self.server_name.clone(), value) + } + Err(e) => { + warn!("failed caching session: unable to encode entry: {:?}", e); + } + } + } + + /// Called by the static callbacks to retrieve the instance pointer. + #[inline] + fn get_instance(ssl: *const bffi::SSL) -> &'static mut Session { + unsafe { + let data = bffi::SSL_get_ex_data(ssl, *SESSION_INDEX); + if data.is_null() { + panic!("BUG: Session instance missing") + } + &mut *(data as *mut Session) + } + } + + /// Raw callback from BoringSSL. + extern "C" fn new_session_callback( + ssl: *mut bffi::SSL, + session: *mut bffi::SSL_SESSION, + ) -> c_int { + let inst = Self::get_instance(ssl); + let session = unsafe { SslSession::from_ptr(session) }; + inst.on_new_session(session); + + // Return 1 to indicate we've taken ownership of the session. + 1 + } +} + +impl crypto::Session for Session { + fn initial_keys(&self, dcid: &ConnectionId, side: Side) -> crypto::Keys { + self.state.initial_keys(dcid, side) + } + + fn handshake_data(&self) -> Option> { + self.state.handshake_data() + } + + fn peer_identity(&self) -> Option> { + self.state.peer_identity() + } + + fn early_crypto(&self) -> Option<(Box, Box)> { + self.state.early_crypto() + } + + fn early_data_accepted(&self) -> Option { + Some(self.state.ssl.early_data_accepted()) + } + + fn is_handshaking(&self) -> bool { + self.state.is_handshaking() + } + + fn read_handshake(&mut self, plaintext: &[u8]) -> StdResult { + self.state.read_handshake(plaintext)?; + + if self.state.early_data_rejected { + self.on_zero_rtt_rejected(); + } + + // Only indicate that handshake data is available once. + // On the client side there is no ALPN callback, so we need to manually check + // if the ALPN protocol has been selected. + if !self.handshake_data_sent { + if self.state.ssl.selected_alpn_protocol().is_some() { + self.handshake_data_available = true; + } + + if self.handshake_data_available { + self.handshake_data_sent = true; + return Ok(true); + } + } + + Ok(false) + } + + fn transport_parameters(&self) -> StdResult, TransportError> { + match self.state.transport_parameters()? { + Some(params) => Ok(Some(params)), + None => { + if self.state.ssl.in_early_data() { + Ok(self.zero_rtt_peer_params) + } else { + Ok(None) + } + } + } + } + + fn write_handshake(&mut self, buf: &mut Vec) -> Option { + self.state.write_handshake(buf) + } + + fn next_1rtt_keys(&mut self) -> Option>> { + self.state.next_1rtt_keys() + } + + fn is_valid_retry(&self, orig_dst_cid: &ConnectionId, header: &[u8], payload: &[u8]) -> bool { + self.state.is_valid_retry(orig_dst_cid, header, payload) + } + + fn export_keying_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> StdResult<(), crypto::ExportKeyingMaterialError> { + self.state.export_keying_material(output, label, context) + } +} + +fn encode_params(params: &TransportParameters) -> Bytes { + let mut out = BytesMut::with_capacity(128); + params.write(&mut out); + out.freeze() +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..aa37bb1 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,150 @@ +use boring::error::ErrorStack; +use quinn_proto::{crypto, ConnectError, TransportError}; +use std::ffi::c_int; +use std::fmt::{Debug, Display, Formatter}; +use std::io::ErrorKind; +use std::result::Result as StdResult; +use std::{fmt, io}; + +// Error conversion: +pub enum Error { + SslError(ErrorStack), + IoError(io::Error), + ConnectError(ConnectError), + TransportError(TransportError), +} + +impl Debug for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::SslError(e) => Debug::fmt(&e, f), + Self::IoError(e) => Debug::fmt(&e, f), + Self::ConnectError(e) => Debug::fmt(&e, f), + Self::TransportError(e) => Debug::fmt(&e, f), + } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::SslError(e) => Display::fmt(&e, f), + Self::IoError(e) => Display::fmt(&e, f), + Self::ConnectError(e) => Display::fmt(&e, f), + Self::TransportError(e) => Display::fmt(&e, f), + } + } +} + +impl std::error::Error for Error {} + +impl Error { + pub(crate) fn ssl() -> Self { + Error::SslError(ErrorStack::get()) + } + + pub(crate) fn invalid_input(msg: String) -> Self { + Error::IoError(io::Error::new(ErrorKind::InvalidInput, msg)) + } + + pub(crate) fn other(msg: String) -> Self { + Error::IoError(io::Error::new(ErrorKind::Other, msg)) + } +} + +/// Support conversion to CryptoError. +impl From for crypto::CryptoError { + fn from(_: Error) -> Self { + crypto::CryptoError + } +} + +/// Support conversion to ConnectError. +impl From for ConnectError { + fn from(e: Error) -> Self { + match e { + Error::SslError(_) => Self::EndpointStopping, + Error::IoError(_) => Self::EndpointStopping, + Error::ConnectError(e) => e, + Error::TransportError(_) => Self::EndpointStopping, + } + } +} + +impl From for Error { + fn from(e: ErrorStack) -> Self { + Error::SslError(e) + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Error::IoError(e) + } +} + +impl From for Error { + fn from(e: ConnectError) -> Self { + Error::ConnectError(e) + } +} + +impl From for Error { + fn from(e: TransportError) -> Self { + Error::TransportError(e) + } +} + +/// The main result type for this (crypto boring) module. +pub type Result = StdResult; + +/// The result returned by the Cloudflare Boring library API functions. +pub(crate) type BoringResult = StdResult<(), ErrorStack>; + +/// Maps BoringSSL ffi return values to the Result type consistent with the Boring APIs. +pub(crate) fn br(bssl_result: c_int) -> BoringResult { + match bssl_result { + 1 => Ok(()), + _ => Err(ErrorStack::get()), + } +} + +pub(crate) fn br_zero_is_success(bssl_result: c_int) -> BoringResult { + match bssl_result { + 0 => Ok(()), + _ => Err(ErrorStack::get()), + } +} + +/// Maps BoringSSL ffi return values to a Result. +pub(crate) fn map_result(bssl_result: c_int) -> Result<()> { + match bssl_result { + 1 => Ok(()), + _ => Err(Error::SslError(ErrorStack::get())), + } +} + +/// Maps a result from a Rust callback to a BoringSSL result error code. +pub(crate) fn map_cb_result(result: Result) -> c_int { + match result { + Ok(_) => 1, + _ => 0, + } +} + +/// Like map_result, but for BoringSSL method that break the standard return value convention. +pub(crate) fn map_result_zero_is_success(bssl_result: c_int) -> Result<()> { + match bssl_result { + 0 => Ok(()), + _ => Err(Error::SslError(ErrorStack::get())), + } +} + +/// Like map_result, but ensures that the resulting pointer is non-null. +pub(crate) fn map_ptr_result(r: *mut T) -> Result<*mut T> { + if r.is_null() { + Err(Error::ssl()) + } else { + Ok(r) + } +} diff --git a/src/handshake_token.rs b/src/handshake_token.rs new file mode 100644 index 0000000..43c4c01 --- /dev/null +++ b/src/handshake_token.rs @@ -0,0 +1,68 @@ +use crate::error::Result; +use crate::hkdf::Hkdf; +use crate::key::{AeadKey, Key}; +use crate::secret::Secret; +use crate::suite::CipherSuite; +use quinn_proto::crypto; + +pub struct HandshakeTokenKey(Key); + +impl HandshakeTokenKey { + /// Creates a new randomized HandshakeTokenKey. + pub fn new() -> Result { + Self::new_for(Secret::random()) + } + + fn new_for(secret: Secret) -> Result { + // Extract the key. + let mut key = [0u8; Key::MAX_LEN]; + let len = Hkdf::sha256().extract(&[], secret.slice(), &mut key)?; + Ok(Self(Key::new(key, len))) + } +} + +impl crypto::HandshakeTokenKey for HandshakeTokenKey { + fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Box { + let suite = CipherSuite::aes256_gcm_sha384(); + let prk = self.0.slice(); + let mut key = suite.aead.zero_key(); + Hkdf::sha256() + .expand(prk, random_bytes, key.slice_mut()) + .unwrap(); + + Box::new(AeadKey::new(suite, key).unwrap()) + } +} + +#[cfg(test)] +mod test { + use hex_literal::hex; + use quinn_proto::crypto::HandshakeTokenKey; + + #[test] + fn round_trip() { + // Create a random token key. + let master_key = hex!("ab35ad55e9957c0e67aedbbd76f6a781528a5b43cc57bfd633" + "ccca412327aa23e0d7140d5fc290d1637746706c7d703e3bf405687a69ee82284a5ede49f59e19"); + let htk = super::HandshakeTokenKey::new_for(super::Secret::from(&master_key)).unwrap(); + + // Generate an AEAD from the given random. + let random_bytes = hex!("b088e52e27da85f8838e163ddb90fd35d633fad44f0ab9c39f05459297178599"); + let aead_key = htk.aead_from_hkdf(&random_bytes); + + // Inputs for the seal/open operations. + let data = hex!("146a6d36221e4f24eda3a16f71a816a8a72dd7efbb0000000064076bde"); + let additional_data = hex!("00000000000000000000000000000001d60c084b239b74c31de86f"); + + // Seal the buffer and verify the expected output. + let mut buf = data.to_vec(); + aead_key.seal(&mut buf, &additional_data).unwrap(); + let expected = hex!("504a8f0841f3fb7dbf8b3df90b5be913cb5a28000918510baeff64" + "5d72b67ab34a47da820a97416d68d0b605af"); + assert_eq!(&expected, buf.as_slice()); + + // Now open and verify we get back the original data. + let out = aead_key.open(&mut buf, &additional_data).unwrap(); + assert_eq!(&data, out); + } +} diff --git a/src/hkdf.rs b/src/hkdf.rs new file mode 100644 index 0000000..82f73d7 --- /dev/null +++ b/src/hkdf.rs @@ -0,0 +1,129 @@ +use crate::error::{map_result, Error, Result}; +use boring::hash::MessageDigest; +use boring_sys as bffi; +use bytes::{BufMut, BytesMut}; +use once_cell::sync::Lazy; + +/// The block size used by the supported digest algorithms (64). +pub(crate) const DIGEST_BLOCK_LEN: usize = bffi::SHA_CBLOCK as _; + +// /// The digest size for SHA256 (32). +// pub(crate) const SHA256_DIGEST_LEN: usize = bffi::SHA256_DIGEST_LENGTH as _; +// +// /// The digest size for SHA384 (48). +// pub(crate) const SHA384_DIGEST_LEN: usize = bffi::SHA384_DIGEST_LENGTH as _; + +/// Implementation of [HKDF](https://www.rfc-editor.org/rfc/rfc5869) used for +/// creating the initial secrets for +/// [QUIC](https://www.rfc-editor.org/rfc/rfc9001#section-5.2) and +/// [TLS_1.3](https://datatracker.ietf.org/doc/html/rfc9001#name-initial-secrets). +#[derive(Clone, Copy, Eq, PartialEq)] +pub(crate) struct Hkdf(MessageDigest); + +static SHA256: Lazy = Lazy::new(|| Hkdf(MessageDigest::sha256())); + +static SHA384: Lazy = Lazy::new(|| Hkdf(MessageDigest::sha384())); + +impl Hkdf { + pub(crate) fn sha256() -> Hkdf { + *SHA256 + } + + pub(crate) fn sha384() -> Hkdf { + *SHA384 + } + + /// The size of the digest in bytes. + #[inline] + pub(crate) fn digest_size(self) -> usize { + self.0.size() + } + + /// Performs an HKDF extract (), + /// given the salt and the initial key material (IKM). Returns the slice of the 'out' + /// array containing the generated pseudorandom key (PRK). + #[inline] + pub(crate) fn extract(self, salt: &[u8], ikm: &[u8], out: &mut [u8]) -> Result { + if out.len() < self.digest_size() { + return Err(Error::invalid_input(format!( + "HKDF extract output array invalid size: {}", + out.len() + ))); + } + + let mut out_len = out.len(); + + unsafe { + map_result(bffi::HKDF_extract( + out.as_mut_ptr(), + &mut out_len, + self.0.as_ptr(), + ikm.as_ptr(), + ikm.len(), + salt.as_ptr(), + salt.len(), + ))?; + + Ok(out_len) + } + } + + /// Performs the HKDF-Expand-Label function as defined in the + /// [TLS-1.3 spec](https://datatracker.ietf.org/doc/html/rfc8446#section-7.1). The + /// [HKDF-Expand-Label function](https://www.rfc-editor.org/rfc/rfc5869#section-2.3) takes + /// 4 explicit arguments (Secret, Label, Context, and Length), as well as implicit PRF + /// which is the hash function negotiated by TLS. + /// + /// Its use in QUIC is only for deriving initial secrets for obfuscation, for calculating + /// packet protection keys and IVs from the corresponding packet protection secret and + /// key update in the same quic session. None of these uses need a Context (a zero-length + /// context is provided), so this argument is omitted here. + #[inline] + pub(crate) fn expand_label(self, secret: &[u8], label: &[u8], out: &mut [u8]) -> Result<()> { + // Convert the label to a structure required by HKDF_expand. Doing + // this inline rather than using the complex openssl Crypto ByteBuilder (CBB). + let label = { + const TLS_VERSION_LABEL: &[u8] = b"tls13 "; + + // Initialize the builder for the label structure. Breakdown of the required capacity: + // 2-byte total length field + + // 1-byte for the length of the label + + // Label length + + // 1-byte for the length of the Context + + // 0-bytes for an empty context (QUIC does not use context). + let label_len = TLS_VERSION_LABEL.len() + label.len(); + let builder_capacity = label_len + 4; + let mut builder = BytesMut::with_capacity(builder_capacity); + + // Add the length of the output key in big-endian byte order. + builder.put_u16(out.len() as u16); + + // Add a child containing the label. + builder.put_u8(label_len as u8); + builder.put(TLS_VERSION_LABEL); + builder.put(label); + + // Add a child containing a zero hash. + builder.put_u8(0); + builder + }; + + self.expand(secret, &label, out) + } + + #[inline] + pub(crate) fn expand(&self, prk: &[u8], info: &[u8], out: &mut [u8]) -> Result<()> { + unsafe { + map_result(bffi::HKDF_expand( + out.as_mut_ptr(), + out.len(), + self.0.as_ptr(), + prk.as_ptr(), + prk.len(), + info.as_ptr(), + info.len(), + ))?; + } + Ok(()) + } +} diff --git a/src/hmac.rs b/src/hmac.rs new file mode 100644 index 0000000..1abd882 --- /dev/null +++ b/src/hmac.rs @@ -0,0 +1,77 @@ +use crate::error::map_ptr_result; +use crate::hkdf::DIGEST_BLOCK_LEN; +use boring::hash::MessageDigest; +use boring_sys as bffi; +use quinn_proto::crypto; +use rand::RngCore; +use std::ffi::{c_uint, c_void}; +use std::result::Result as StdResult; + +const SIGNATURE_LEN_SHA_256: usize = 32; + +/// Implementation of [crypto::HmacKey] using BoringSSL. +pub struct HmacKey { + alg: MessageDigest, + key: Vec, +} + +impl HmacKey { + /// Creates a new randomized SHA-256 HMAC key. + pub fn sha256() -> Self { + // Create a random key. + let mut key = [0u8; DIGEST_BLOCK_LEN]; + rand::thread_rng().fill_bytes(&mut key); + + Self { + alg: MessageDigest::sha256(), + key: Vec::from(key), + } + } +} + +impl crypto::HmacKey for HmacKey { + fn sign(&self, data: &[u8], out: &mut [u8]) { + let mut out_len = out.len() as c_uint; + unsafe { + map_ptr_result(bffi::HMAC( + self.alg.as_ptr(), + self.key.as_ptr() as *const c_void, + self.key.len(), + data.as_ptr(), + data.len(), + out.as_mut_ptr(), + &mut out_len, + )) + .unwrap(); + } + + // Verify the signature length. + if out_len as usize != self.signature_len() { + panic!( + "HMAC.sign: generated signature with unexpected length: {}", + out_len + ); + } + } + + #[inline] + fn signature_len(&self) -> usize { + SIGNATURE_LEN_SHA_256 + } + + fn verify(&self, data: &[u8], signature: &[u8]) -> StdResult<(), crypto::CryptoError> { + if signature.len() != self.signature_len() { + return Err(crypto::CryptoError {}); + } + + // Sign the data. + let mut out = [0u8; SIGNATURE_LEN_SHA_256]; + self.sign(data, &mut out); + + // Compare the output. + if out == signature { + return Ok(()); + } + Err(crypto::CryptoError {}) + } +} diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 0000000..424743b --- /dev/null +++ b/src/key.rs @@ -0,0 +1,525 @@ +use crate::error::{map_result, map_result_zero_is_success, Result}; +use crate::macros::bounded_array; +use crate::secret::Secret; +use crate::suite::{CipherSuite, ID}; +use crate::{Error, QuicVersion}; +use boring_sys as bffi; +use bytes::BytesMut; +use quinn_proto::crypto; +use std::ffi::c_uint; +use std::fmt::{Debug, Formatter}; +use std::mem; +use std::mem::MaybeUninit; +use std::result::Result as StdResult; + +const SAMPLE_LEN: usize = 16; // 128-bits. + +/// The maximum key size used by Quic algorithms. +const MAX_KEY_LEN: usize = 32; + +/// The maximum nonce size used by Quic algorithms. +const MAX_NONCE_LEN: usize = 12; + +/// The maximum tag size used by Quic algorithms. +const MAX_TAG_LEN: usize = 16; + +bounded_array! { + /// A buffer that can fit the largest key supported by Quic. + pub(crate) struct Key(MAX_KEY_LEN), + + /// A buffer that can fit the largest nonce supported by Quic. + pub(crate) struct Nonce(MAX_NONCE_LEN), + + /// A buffer that can fit the largest tag supported by Quic. + pub(crate) struct Tag(MAX_TAG_LEN) +} + +/// A pair of keys for bidirectional communication +#[derive(Copy, Clone, Debug)] +pub(crate) struct KeyPair { + /// The key for this side, used for encrypting data. + pub(crate) local: T, + + /// The key for the other side, used for decrypting data. + pub(crate) remote: T, +} + +impl KeyPair { + #[inline] + pub(crate) fn as_crypto(&self) -> Result>> { + Ok(crypto::KeyPair { + local: self.local.as_crypto()?, + remote: self.remote.as_crypto()?, + }) + } +} + +impl KeyPair { + #[inline] + pub(crate) fn as_crypto(&self) -> Result>> { + Ok(crypto::KeyPair { + local: Box::new(self.local), + remote: Box::new(self.remote), + }) + } +} + +/// A complete set of keys for a certain encryption level. +#[derive(Clone, Debug)] +pub(crate) struct Keys { + /// Header protection keys + pub(crate) header: KeyPair, + /// Packet protection keys + pub(crate) packet: KeyPair, +} + +impl Keys { + pub(crate) fn as_crypto(&self) -> Result { + Ok(crypto::Keys { + header: self.header.as_crypto()?, + packet: self.packet.as_crypto()?, + }) + } +} + +/// Internal header key representation. Supports conversion to [crypto::HeaderKey] +#[derive(Copy, Clone, Debug)] +pub(crate) struct HeaderKey { + suite: &'static CipherSuite, + key: Key, +} + +impl HeaderKey { + pub(crate) fn new( + version: QuicVersion, + suite: &'static CipherSuite, + secret: &Secret, + ) -> Result { + let mut key = suite.aead.zero_key(); + suite + .hkdf + .expand_label(secret.slice(), version.header_key_label(), key.slice_mut())?; + + Ok(Self { suite, key }) + } + + #[inline] + pub(crate) fn key(&self) -> &Key { + &self.key + } + + /// Converts to a crypto HeaderKey. + #[inline] + pub(crate) fn as_crypto(&self) -> Result> { + match self.suite.id { + ID::Aes128GcmSha256 | ID::Aes256GcmSha384 => { + Ok(Box::new(AesHeaderKey::new(self.key())?)) + } + ID::Chacha20Poly1305Sha256 => Ok(Box::new(ChaChaHeaderKey::new(self.key())?)), + } + } +} + +/// Base trait for a crypto header protection keys. Implementation copied from rustls. +trait CryptoHeaderKey: crypto::HeaderKey { + fn new_mask(&self, sample: &[u8]) -> Result<[u8; 5]>; + + #[inline] + fn sample_len(&self) -> usize { + SAMPLE_LEN + } + + #[inline] + fn decrypt_in_place(&self, pn_offset: usize, packet: &mut [u8]) { + let (header, sample) = packet.split_at_mut(pn_offset + 4); + let (first, rest) = header.split_at_mut(1); + let pn_end = Ord::min(pn_offset + 3, rest.len()); + self.xor_in_place( + &sample[..self.sample_len()], + &mut first[0], + &mut rest[pn_offset - 1..pn_end], + true, + ) + .unwrap(); + } + + #[inline] + fn encrypt_in_place(&self, pn_offset: usize, packet: &mut [u8]) { + let (header, sample) = packet.split_at_mut(pn_offset + 4); + let (first, rest) = header.split_at_mut(1); + let pn_end = Ord::min(pn_offset + 3, rest.len()); + self.xor_in_place( + &sample[..self.sample_size()], + &mut first[0], + &mut rest[pn_offset - 1..pn_end], + false, + ) + .unwrap(); + } + + #[inline] + fn xor_in_place( + &self, + sample: &[u8], + first: &mut u8, + packet_number: &mut [u8], + masked: bool, + ) -> Result<()> { + // This implements [Header Protection Application] almost verbatim. + + let mask = self.new_mask(sample).unwrap(); + + // The `unwrap()` will not panic because `new_mask` returns a + // non-empty result. + let (first_mask, pn_mask) = mask.split_first().unwrap(); + + // It is OK for the `mask` to be longer than `packet_number`, + // but a valid `packet_number` will never be longer than `mask`. + if packet_number.len() > pn_mask.len() { + return Err(Error::other(format!( + "packet number too long: {}", + packet_number.len() + ))); + } + + // Infallible from this point on. Before this point, `first` and + // `packet_number` are unchanged. + + const LONG_HEADER_FORM: u8 = 0x80; + let bits = match *first & LONG_HEADER_FORM == LONG_HEADER_FORM { + true => 0x0f, // Long header: 4 bits masked + false => 0x1f, // Short header: 5 bits masked + }; + + let first_plain = match masked { + // When unmasking, use the packet length bits after unmasking + true => *first ^ (first_mask & bits), + // When masking, use the packet length bits before masking + false => *first, + }; + let pn_len = (first_plain & 0x03) as usize + 1; + + *first ^= first_mask & bits; + for (dst, m) in packet_number.iter_mut().zip(pn_mask).take(pn_len) { + *dst ^= m; + } + + Ok(()) + } +} + +/// A [CryptoHeaderKey] for AES ciphers. +struct AesHeaderKey(bffi::AES_KEY); + +impl AesHeaderKey { + fn new(key: &Key) -> Result { + let hpk = unsafe { + let mut hpk = MaybeUninit::uninit(); + + // NOTE: this function breaks the usual return value convention. + map_result_zero_is_success(bffi::AES_set_encrypt_key( + key.as_ptr(), + (key.len() * 8) as c_uint, + hpk.as_mut_ptr(), + ))?; + + hpk.assume_init() + }; + Ok(Self(hpk)) + } +} + +impl CryptoHeaderKey for AesHeaderKey { + #[inline] + fn new_mask(&self, sample: &[u8]) -> Result<[u8; 5]> { + if sample.len() != SAMPLE_LEN { + return Err(Error::invalid_input(format!( + "invalid sample length: {}", + sample.len() + ))); + } + + let mut encrypted: [u8; SAMPLE_LEN] = [0; SAMPLE_LEN]; + unsafe { + bffi::AES_encrypt(sample.as_ptr(), encrypted.as_mut_ptr(), &self.0); + } + + let mut out: [u8; 5] = [0; 5]; + out.copy_from_slice(&encrypted[..5]); + Ok(out) + } +} + +impl crypto::HeaderKey for AesHeaderKey { + #[inline] + fn decrypt(&self, pn_offset: usize, packet: &mut [u8]) { + self.decrypt_in_place(pn_offset, packet) + } + + #[inline] + fn encrypt(&self, pn_offset: usize, packet: &mut [u8]) { + self.encrypt_in_place(pn_offset, packet) + } + + #[inline] + fn sample_size(&self) -> usize { + self.sample_len() + } +} + +/// A [CryptoHeaderKey] for ChaCha ciphers. +struct ChaChaHeaderKey(Key); + +impl ChaChaHeaderKey { + const ZEROS: [u8; 5] = [0; 5]; + + fn new(key: &Key) -> Result { + Ok(Self(*key)) + } +} + +impl CryptoHeaderKey for ChaChaHeaderKey { + #[inline] + fn new_mask(&self, sample: &[u8]) -> Result<[u8; 5]> { + if sample.len() != SAMPLE_LEN { + return Err(Error::invalid_input(format!( + "sample len invalid: {}", + sample.len() + ))); + } + + // Extract the counter and the nonce from the sample. + let (counter, nonce) = sample.split_at(mem::size_of::()); + let counter = u32::from_ne_bytes(counter.try_into().unwrap()); + + let mut out: [u8; 5] = [0; 5]; + unsafe { + bffi::CRYPTO_chacha_20( + out.as_mut_ptr(), + Self::ZEROS.as_ptr(), + Self::ZEROS.len(), + self.0.as_ptr(), + nonce.as_ptr(), + counter, + ); + } + + Ok(out) + } +} + +impl crypto::HeaderKey for ChaChaHeaderKey { + #[inline] + fn decrypt(&self, pn_offset: usize, packet: &mut [u8]) { + self.decrypt_in_place(pn_offset, packet) + } + + #[inline] + fn encrypt(&self, pn_offset: usize, packet: &mut [u8]) { + self.encrypt_in_place(pn_offset, packet) + } + + #[inline] + fn sample_size(&self) -> usize { + self.sample_len() + } +} + +/// Internal key representation. +#[derive(Copy, Clone, Debug)] +pub(crate) struct PacketKey { + aead_key: AeadKey, + iv: Nonce, +} + +impl PacketKey { + #[inline] + pub(crate) fn new( + version: QuicVersion, + suite: &'static CipherSuite, + secret: &Secret, + ) -> Result { + let mut key = suite.aead.zero_key(); + suite + .hkdf + .expand_label(secret.slice(), version.key_label(), key.slice_mut())?; + + let mut iv = suite.aead.zero_nonce(); + suite + .hkdf + .expand_label(secret.slice(), version.iv_label(), iv.slice_mut())?; + + let aead_key = AeadKey::new(suite, key)?; + + Ok(Self { aead_key, iv }) + } + + #[cfg(test)] + pub(crate) fn key(&self) -> &Key { + &self.aead_key.key + } + + #[cfg(test)] + pub(crate) fn iv(&self) -> &Nonce { + &self.iv + } + + #[inline] + fn nonce_for_packet(&self, packet_number: u64) -> Nonce { + let mut nonce = self.aead_key.suite.aead.zero_nonce(); + let slice = nonce.slice_mut(); + slice[4..].copy_from_slice(&packet_number.to_be_bytes()); + for (out, inp) in slice.iter_mut().zip(self.iv.slice().iter()) { + *out ^= inp; + } + nonce + } +} + +impl crypto::PacketKey for PacketKey { + /// Encrypt a QUIC packet in-place. + fn encrypt(&self, packet_number: u64, buf: &mut [u8], header_len: usize) { + let (header, payload_tag) = buf.split_at_mut(header_len); + + let nonce = self.nonce_for_packet(packet_number); + + self.aead_key + .seal_in_place(&nonce, payload_tag, header) + .unwrap(); + } + + /// Decrypt a QUIC packet in-place. + fn decrypt( + &self, + packet_number: u64, + header: &[u8], + payload: &mut BytesMut, + ) -> StdResult<(), crypto::CryptoError> { + let nonce = self.nonce_for_packet(packet_number); + + let plain_len = self + .aead_key + .open_in_place(&nonce, payload.as_mut(), header)?; + payload.truncate(plain_len); + Ok(()) + } + + #[inline] + fn tag_len(&self) -> usize { + self.aead_key.suite.aead.tag_len + } + + #[inline] + fn confidentiality_limit(&self) -> u64 { + self.aead_key.suite.confidentiality_limit + } + + #[inline] + fn integrity_limit(&self) -> u64 { + self.aead_key.suite.integrity_limit + } +} + +/// A [crypto::PacketKey] that is based on a BoringSSL [bffi::EVP_AEAD_CTX]. +#[derive(Copy, Clone)] +pub(crate) struct AeadKey { + suite: &'static CipherSuite, + key: Key, + ctx: bffi::EVP_AEAD_CTX, +} + +impl Debug for AeadKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AeadKey") + .field("suite", self.suite) + .field("key", &self.key) + .finish() + } +} + +unsafe impl Send for AeadKey {} + +impl AeadKey { + #[inline] + pub(crate) fn new(suite: &'static CipherSuite, key: Key) -> Result { + let ctx = suite.aead.new_aead_ctx(&key)?; + Ok(Self { suite, key, ctx }) + } + + #[inline] + pub(crate) fn seal_in_place( + &self, + nonce: &Nonce, + data: &mut [u8], + additional_data: &[u8], + ) -> Result<()> { + let mut out_len = data.len() - self.suite.aead.tag_len; + unsafe { + map_result(bffi::EVP_AEAD_CTX_seal( + &self.ctx, + data.as_mut_ptr(), + &mut out_len, + data.len(), + nonce.as_ptr(), + nonce.len(), + data.as_ptr(), + out_len, + additional_data.as_ptr(), + additional_data.len(), + ))?; + } + Ok(()) + } + + #[inline] + pub(crate) fn open_in_place( + &self, + nonce: &Nonce, + data: &mut [u8], + additional_data: &[u8], + ) -> StdResult { + let mut out_len = match data.len().checked_sub(self.suite.aead.tag_len) { + Some(n) => n, + None => return Err(crypto::CryptoError {}), + }; + + unsafe { + map_result(bffi::EVP_AEAD_CTX_open( + &self.ctx, + data.as_mut_ptr(), + &mut out_len, + out_len, + nonce.as_ptr(), + nonce.len(), + data.as_ptr(), + data.len(), + additional_data.as_ptr(), + additional_data.len(), + ))?; + } + Ok(out_len) + } +} + +impl crypto::AeadKey for AeadKey { + #[inline] + fn seal( + &self, + data: &mut Vec, + additional_data: &[u8], + ) -> StdResult<(), crypto::CryptoError> { + data.extend_from_slice(self.suite.aead.zero_tag().slice()); + self.seal_in_place(&self.suite.aead.zero_nonce(), data, additional_data)?; + Ok(()) + } + + #[inline] + fn open<'a>( + &self, + data: &'a mut [u8], + additional_data: &[u8], + ) -> StdResult<&'a mut [u8], crypto::CryptoError> { + let plain_len = self.open_in_place(&self.suite.aead.zero_nonce(), data, additional_data)?; + Ok(&mut data[..plain_len]) + } +} diff --git a/src/key_log.rs b/src/key_log.rs new file mode 100644 index 0000000..e155f49 --- /dev/null +++ b/src/key_log.rs @@ -0,0 +1,90 @@ +use crate::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::str::FromStr; + +const CLIENT_RANDOM: &str = "CLIENT_RANDOM"; +const CLIENT_EARLY_TRAFFIC_SECRET: &str = "CLIENT_EARLY_TRAFFIC_SECRET"; +const CLIENT_HANDSHAKE_TRAFFIC_SECRET: &str = "CLIENT_HANDSHAKE_TRAFFIC_SECRET"; +const SERVER_HANDSHAKE_TRAFFIC_SECRET: &str = "SERVER_HANDSHAKE_TRAFFIC_SECRET"; +const CLIENT_TRAFFIC_SECRET_0: &str = "CLIENT_TRAFFIC_SECRET_0"; +const SERVER_TRAFFIC_SECRET_0: &str = "SERVER_TRAFFIC_SECRET_0"; +const EXPORTER_SECRET: &str = "EXPORTER_SECRET"; + +/// Enumeration of the possible values for the keylog label. +/// See +/// for details. +#[derive(Eq, PartialEq)] +pub enum KeyLogLabel { + ClientRandom, + ClientEarlyTrafficSecret, + ClientHandshakeTrafficSecret, + ServerHandshakeTrafficSecret, + ClientTrafficSecret0, + ServerTrafficSecret0, + ExporterSecret, +} + +impl KeyLogLabel { + pub fn to_str(&self) -> &'static str { + match self { + KeyLogLabel::ClientRandom => CLIENT_RANDOM, + KeyLogLabel::ClientEarlyTrafficSecret => CLIENT_EARLY_TRAFFIC_SECRET, + KeyLogLabel::ClientHandshakeTrafficSecret => CLIENT_HANDSHAKE_TRAFFIC_SECRET, + KeyLogLabel::ServerHandshakeTrafficSecret => SERVER_HANDSHAKE_TRAFFIC_SECRET, + KeyLogLabel::ClientTrafficSecret0 => CLIENT_TRAFFIC_SECRET_0, + KeyLogLabel::ServerTrafficSecret0 => SERVER_TRAFFIC_SECRET_0, + KeyLogLabel::ExporterSecret => EXPORTER_SECRET, + } + } +} + +impl Debug for KeyLogLabel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_str()) + } +} + +impl Display for KeyLogLabel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_str()) + } +} + +impl FromStr for KeyLogLabel { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + CLIENT_RANDOM => Ok(Self::ClientRandom), + CLIENT_EARLY_TRAFFIC_SECRET => Ok(Self::ClientEarlyTrafficSecret), + CLIENT_HANDSHAKE_TRAFFIC_SECRET => Ok(Self::ClientHandshakeTrafficSecret), + SERVER_HANDSHAKE_TRAFFIC_SECRET => Ok(Self::ServerHandshakeTrafficSecret), + CLIENT_TRAFFIC_SECRET_0 => Ok(Self::ClientTrafficSecret0), + SERVER_TRAFFIC_SECRET_0 => Ok(Self::ServerTrafficSecret0), + EXPORTER_SECRET => Ok(Self::ExporterSecret), + _ => Err(Error::invalid_input(format!( + "unable to parse keylog label: {}", + s + ))), + } + } +} + +/// Provides a handler for logging key material. This is intended for debugging use with +/// tools like Wireshark. +pub trait KeyLog: Send + Sync { + /// Logs the given `secret`. `client_random` is provided for + /// session identification. `label` describes precisely what + /// `secret` means. + /// + /// Details of the format are described in: + /// + fn log_key(&self, label: KeyLogLabel, client_random: &str, secret: &str); +} + +/// A [KeyLog] that does nothing. +pub struct NoKeyLog; + +impl KeyLog for NoKeyLog { + fn log_key(&self, _: KeyLogLabel, _: &str, _: &str) {} +} diff --git a/src/lib.rs b/src/lib.rs index e69de29..8cf4a28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -0,0 +1,296 @@ +mod aead; +mod alert; +mod alpn; +mod bffi_ext; +mod client; +mod error; +mod handshake_token; +mod hkdf; +mod hmac; +mod key; +mod key_log; +mod macros; +mod retry; +mod secret; +mod server; +mod session_cache; +mod session_state; +mod suite; +mod version; + +// Export the public interface. +pub use bffi_ext::*; +pub use client::Config as ClientConfig; +pub use error::{Error, Result}; +pub use handshake_token::HandshakeTokenKey; +pub use hmac::HmacKey; +pub use key_log::*; +pub use server::Config as ServerConfig; +pub use session_cache::*; +pub use version::QuicVersion; + +/// Information available from [quinn_proto::crypto::Session::handshake_data] once the handshake has completed. +#[derive(Clone, Debug)] +pub struct HandshakeData { + /// The negotiated application protocol, if ALPN is in use + /// + /// Guaranteed to be set if a nonempty list of protocols was specified for this connection. + pub protocol: Option>, + + /// The server name specified by the client, if any + /// + /// Always `None` for outgoing connections + pub server_name: Option, +} + +pub mod helpers { + use super::*; + use quinn::TokioRuntime; + use quinn_proto::crypto; + use std::io; + use std::net::SocketAddr; + use std::sync::Arc; + + /// Create a server config with the given [`crypto::ServerConfig`] + /// + /// Uses a randomized handshake token key. + pub fn server_config(crypto: Arc) -> Result { + Ok(quinn::ServerConfig::new( + crypto, + Arc::new(HandshakeTokenKey::new()?), + )) + } + + /// Returns a default endpoint configuration for BoringSSL. + pub fn default_endpoint_config() -> quinn::EndpointConfig { + let mut cfg = quinn::EndpointConfig::new(Arc::new(HmacKey::sha256())); + cfg.supported_versions(QuicVersion::default_supported_versions()); + cfg + } + + /// Helper to construct an endpoint for use with outgoing connections only + /// + /// Note that `addr` is the *local* address to bind to, which should usually be a wildcard + /// address like `0.0.0.0:0` or `[::]:0`, which allow communication with any reachable IPv4 or + /// IPv6 address respectively from an OS-assigned port. + /// + /// Platform defaults for dual-stack sockets vary. For example, any socket bound to a wildcard + /// IPv6 address on Windows will not by default be able to communicate with IPv4 + /// addresses. Portable applications should bind an address that matches the family they wish to + /// communicate within. + pub fn client_endpoint(addr: SocketAddr) -> io::Result { + let socket = std::net::UdpSocket::bind(addr)?; + quinn::Endpoint::new(default_endpoint_config(), None, socket, TokioRuntime) + } + + /// Helper to construct an endpoint for use with both incoming and outgoing connections + /// + /// Platform defaults for dual-stack sockets vary. For example, any socket bound to a wildcard + /// IPv6 address on Windows will not by default be able to communicate with IPv4 + /// addresses. Portable applications should bind an address that matches the family they wish to + /// communicate within. + pub fn server_endpoint( + config: quinn::ServerConfig, + addr: SocketAddr, + ) -> io::Result { + let socket = std::net::UdpSocket::bind(addr)?; + quinn::Endpoint::new( + default_endpoint_config(), + Some(config), + socket, + TokioRuntime, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::error::Result; + use crate::secret::{Secret, Secrets}; + use crate::suite::CipherSuite; + use bytes::BytesMut; + use hex_literal::hex; + use quinn_proto::crypto::PacketKey; + use quinn_proto::{ConnectionId, Side}; + + /// Copied from quiche. + #[test] + fn test_initial_keys_v1() -> Result<()> { + let dcid: &[u8] = &hex!("8394c8f03e515708"); + let version = QuicVersion::V1; + let suite = CipherSuite::aes128_gcm_sha256(); + + let s = Secrets::initial(version, &ConnectionId::new(dcid), Side::Client)?; + + let expected_enc_key: &[u8] = &hex!("1f369613dd76d5467730efcbe3b1a22d"); + assert_eq!( + s.local.packet_key(version, suite)?.key().slice(), + expected_enc_key + ); + let expected_enc_iv: &[u8] = &hex!("fa044b2f42a3fd3b46fb255c"); + assert_eq!( + s.local.packet_key(version, suite)?.iv().slice(), + expected_enc_iv + ); + let expected_enc_hdr_key: &[u8] = &hex!("9f50449e04a0e810283a1e9933adedd2"); + assert_eq!( + s.local.header_key(version, suite)?.key().slice(), + expected_enc_hdr_key + ); + let expected_dec_key: &[u8] = &hex!("cf3a5331653c364c88f0f379b6067e37"); + assert_eq!( + s.remote.packet_key(version, suite)?.key().slice(), + expected_dec_key + ); + let expected_dec_iv: &[u8] = &hex!("0ac1493ca1905853b0bba03e"); + assert_eq!( + s.remote.packet_key(version, suite)?.iv().slice(), + expected_dec_iv + ); + let expected_dec_hdr_key: &[u8] = &hex!("c206b8d9b9f0f37644430b490eeaa314"); + assert_eq!( + s.remote.header_key(version, suite)?.key().slice(), + expected_dec_hdr_key + ); + + Ok(()) + } + + /// Copied from rustls. + #[test] + fn short_packet_header_protection() { + // https://www.rfc-editor.org/rfc/rfc9001.html#name-chacha20-poly1305-short-hea + + const PN: u64 = 654360564; + const SECRET: &[u8] = + &hex!("9ac312a7f877468ebe69422748ad00a15443f18203a07d6060f688f30f21632b"); + + let version = QuicVersion::V1; + let suite = CipherSuite::chacha20_poly1305_sha256(); + + let secret = Secret::from(SECRET); + let hpk = secret + .header_key(version, suite) + .unwrap() + .as_crypto() + .unwrap(); + let packet = secret.packet_key(version, suite).unwrap(); + + const PLAIN: &[u8] = &[0x42, 0x00, 0xbf, 0xf4, b'h', b'e', b'l', b'l', b'o']; + + let mut buf = PLAIN.to_vec(); + // Make space for the output tag. + buf.extend_from_slice(&[0u8; 16]); + packet.encrypt(PN, &mut buf, 4); + + let pn_offset = 1; + hpk.encrypt(pn_offset, &mut buf); + + const PROTECTED: &[u8] = &hex!("593b46220c4d504a9f1857793356400fc4a784ee309dff98b2"); + + assert_eq!(&buf, PROTECTED); + + hpk.decrypt(pn_offset, &mut buf); + + let (header, payload_tag) = buf.split_at(4); + let mut payload_tag = BytesMut::from(payload_tag); + packet.decrypt(PN, header, &mut payload_tag).unwrap(); + let plain = payload_tag.as_ref(); + assert_eq!(plain, &PLAIN[4..]); + } + + /// Copied from rustls. + #[test] + fn key_update_test_vector() { + let version = QuicVersion::V1; + let suite = CipherSuite::aes128_gcm_sha256(); + let mut secrets = Secrets { + version, + suite, + local: Secret::from(&hex!( + "b8767708f8772358a6ea9fc43e4add2c961b3f5287a6d1467ee0aeab33724dbf" + )), + remote: Secret::from(&hex!( + "42dc972140e0f2e39845b767613439dc6758ca43259b878506824eb1e438d855" + )), + }; + secrets.update().unwrap(); + + let expected = Secrets { + version, + suite, + local: Secret::from(&hex!( + "42cac8c91cd5eb40682e432edf2d2be9f41a52ca6b22d8e6cdb1e8aca9061fce" + )), + remote: Secret::from(&hex!( + "eb7f5e2a123f407db499e361cae590d4d992e14b7ace03c244e0422115b6d38a" + )), + }; + + assert_eq!(expected, secrets); + } + + #[test] + fn client_encrypt_header() { + let dcid = ConnectionId::new(&hex!("06b858ec6f80452b")); + + let secrets = Secrets::initial(QuicVersion::V1, &dcid, Side::Client).unwrap(); + let client = secrets.keys().unwrap().as_crypto().unwrap(); + + // Client (encrypt) + let mut packet: [u8; 51] = hex!( + "c0000000010806b858ec6f80452b0000402100c8fb7ffd97230e38b70d86e7ff148afdf88fc21c4426c7d1cec79914c8785757" + ); + let packet_number = 0; + let packet_number_pos = 18; + let header_len = 19; + + // Encrypt the payload. + client + .packet + .local + .encrypt(packet_number, &mut packet, header_len); + let expected_after_packet_encrypt: [u8; 51] = hex!( + "c0000000010806b858ec6f80452b0000402100f60e77fa2f629f9921fae64125c5632cf769d801a4693af6b949af37c2c45399" + ); + assert_eq!(packet, expected_after_packet_encrypt); + + // Encrypt the header. + client.header.local.encrypt(packet_number_pos, &mut packet); + let expected_after_header_encrypt: [u8; 51] = hex!( + "cd000000010806b858ec6f80452b000040210bf60e77fa2f629f9921fae64125c5632cf769d801a4693af6b949af37c2c45399" + ); + assert_eq!(packet, expected_after_header_encrypt); + } + + #[test] + fn server_decrypt_header() { + let dcid = ConnectionId::new(&hex!("06b858ec6f80452b")); + let secrets = Secrets::initial(QuicVersion::V1, &dcid, Side::Server).unwrap(); + let server = secrets.keys().unwrap().as_crypto().unwrap(); + + let mut packet = BytesMut::from(&hex!( + "c8000000010806b858ec6f80452b00004021be3ef50807b84191a196f760a6dad1e9d1c430c48952cba0148250c21c0a6a70e1" + )[..]); + let packet_number = 0; + let packet_number_pos = 18; + let header_len = 19; + + // Decrypt the header. + server.header.remote.decrypt(packet_number_pos, &mut packet); + let expected_header: [u8; 19] = hex!("c0000000010806b858ec6f80452b0000402100"); + assert_eq!(packet[..header_len], expected_header); + + // Decrypt the payload. + let mut header = packet; + let mut packet = header.split_off(header_len); + server + .packet + .remote + .decrypt(packet_number, &header, &mut packet) + .unwrap(); + assert_eq!(packet[..], [0; 16]); + } +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..74d9f7d --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,120 @@ +macro_rules! bounded_array { + {$( + $(#[$struct_docs:meta])* + $vis:vis struct $struct_name:ident($max_len:ident) + ),*} => { + $( + $(#[$struct_docs])* + #[derive(Copy, Clone, Eq, PartialEq)] + $vis struct $struct_name { + buf: [u8; Self::MAX_LEN], + len: u8, + } + + #[allow(dead_code)] + impl $struct_name { + /// Maximum value allowed. + $vis const MAX_LEN: usize = $max_len; + + /// Creates a new instance, taking ownership of the buffer. + #[inline] + $vis fn new(buf: [u8; Self::MAX_LEN], len: usize) -> Self { + Self { buf, len: len as _ } + } + + /// Creates a new instance with an empty buffer of the given size. + #[inline] + $vis fn with_len(len: usize) -> Self { + Self::new([0u8; Self::MAX_LEN], len) + } + + /// Creates a new instance, copying the buffer. + #[inline] + $vis fn from(input: &[u8]) -> Self { + assert!(input.len() <= Self::MAX_LEN); + let mut buf = [0u8; Self::MAX_LEN]; + let len = input.len(); + buf[..len].copy_from_slice(input); + + Self::new(buf, len) + } + + /// Creates a new instance with random contents. + #[inline] + $vis fn random() -> Self { + let mut buf = [0u8; Self::MAX_LEN]; + rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut buf); + Self::new(buf, Self::MAX_LEN) + } + + /// Creates a new instance from the parsed hex string. + #[inline] + $vis fn parse_hex_string( + input: &str, + ) -> crate::error::Result { + if input.len() % 2 != 0 { + return Err(crate::error::Error::invalid_input( + "hex string with odd length".to_string(), + )); + } + + let out_len = input.len() / 2; + if out_len > Self::MAX_LEN { + return Err(crate::error::Error::invalid_input( + "hex string value exceeds buffer size".to_string(), + )); + } + + let mut out = [0u8; Self::MAX_LEN]; + + let mut out_ix = 0; + let mut in_ix = 0; + while in_ix < input.len() { + let next_two_chars = &input[in_ix..in_ix + 2]; + out[out_ix] = u8::from_str_radix(next_two_chars, 16).unwrap(); + + in_ix += 2; + out_ix += 1; + } + + Ok($struct_name { + buf: out, + len: out_len as _, + }) + } + + /// Returns the length of the buffer. + #[inline] + $vis fn len(&self) -> usize { + self.len as _ + } + + /// Returns a slice of the buffer for its length. + #[inline] + $vis fn slice(&self) -> &[u8] { + &self.buf[..self.len as _] + } + + /// Returns a mutable slice of the buffer for its length. + #[inline] + $vis fn slice_mut(&mut self) -> &mut [u8] { + &mut self.buf[..self.len as _] + } + + /// Returns a raw pointer to the buffer. + #[inline] + $vis fn as_ptr(&self) -> *const u8 { + self.buf.as_ptr() + } + } + + impl std::fmt::Debug for $struct_name { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:02x?}", self.slice()) + } + } + )* + } +} + +pub(crate) use bounded_array; diff --git a/src/retry.rs b/src/retry.rs new file mode 100644 index 0000000..62dbad2 --- /dev/null +++ b/src/retry.rs @@ -0,0 +1,98 @@ +use crate::key::{AeadKey, Key, Nonce}; +use crate::suite::CipherSuite; +use crate::{aead, QuicVersion}; +use quinn_proto::ConnectionId; + +const TAG_LEN: usize = aead::AES_GCM_TAG_LEN; + +#[inline] +pub(crate) fn retry_tag( + version: &QuicVersion, + orig_dst_cid: &ConnectionId, + packet: &[u8], +) -> [u8; TAG_LEN] { + let suite = CipherSuite::aes128_gcm_sha256(); + let key = Key::from(version.retry_integrity_key()); + let nonce = Nonce::from(version.retry_integrity_nonce()); + let key = AeadKey::new(suite, key).unwrap(); + + let mut pseudo_packet = Vec::with_capacity(packet.len() + orig_dst_cid.len() + 1); + pseudo_packet.push(orig_dst_cid.len() as u8); + pseudo_packet.extend_from_slice(orig_dst_cid); + pseudo_packet.extend_from_slice(packet); + + // Encrypt using the packet as additional data. + let mut encrypted = Vec::from(&[0; TAG_LEN][..]); + key.seal_in_place(&nonce, &mut encrypted, &pseudo_packet) + .unwrap(); + let tag_start = encrypted.len() - TAG_LEN; + + // Now extract the tag that was written. + let mut tag = [0; TAG_LEN]; + tag.copy_from_slice(&encrypted[tag_start..]); + tag +} + +#[inline] +pub(crate) fn is_valid_retry( + version: &QuicVersion, + orig_dst_cid: &ConnectionId, + header: &[u8], + payload: &[u8], +) -> bool { + let tag_start = match payload.len().checked_sub(TAG_LEN) { + Some(x) => x, + None => return false, + }; + + let mut pseudo_packet = + Vec::with_capacity(header.len() + payload.len() + orig_dst_cid.len() + 1); + pseudo_packet.push(orig_dst_cid.len() as u8); + pseudo_packet.extend_from_slice(orig_dst_cid); + pseudo_packet.extend_from_slice(header); + let tag_start = tag_start + pseudo_packet.len(); + pseudo_packet.extend_from_slice(payload); + + let suite = CipherSuite::aes128_gcm_sha256(); + let key = Key::from(version.retry_integrity_key()); + let nonce = Nonce::from(version.retry_integrity_nonce()); + let key = AeadKey::new(suite, key).unwrap(); + + let (aad, tag) = pseudo_packet.split_at_mut(tag_start); + key.open_in_place(&nonce, tag, aad).is_ok() +} + +#[cfg(test)] +mod test { + use super::*; + use hex_literal::hex; + use quinn_proto::ConnectionId; + + #[test] + fn test_is_valid_retry() { + let orig_dst_cid = ConnectionId::new(&hex!("e080ab63f82458c1fd4d64f66faa9216f3f8b481")); + let header = hex!("f0000000010884d5a4bdfc1811e108648f4abb039d0c0a"); + let packet = hex!("e9088adb79f9" + "7eabc8b5c8e78f4cc23da7a9dfa43a48a9b2dedc00c3a928ce501e2067300f1be896c2bde90af634ea8a" + "7fd1bb7ffd7c5ba7087cdb8c2a060eb360017e850bf5d27b063eedffa9" + "dfcdb8ebb4499c60cd86a84a9b2a2adf"); + assert!(is_valid_retry( + &QuicVersion::V1, + &orig_dst_cid, + &header, + &packet + )) + } + + #[test] + fn test_retry_tag() { + let orig_dst_cid = ConnectionId::new(&hex!("e080ab63f82458c1fd4d64f66faa9216f3f8b481")); + let packet = hex!("f0000000010884d5a4bdfc1811e108648f4abb039d0c0ae9088adb79f9" + "7eabc8b5c8e78f4cc23da7a9dfa43a48a9b2dedc00c3a928ce501e2067300f1be896c2bde90af634ea8a" + "7fd1bb7ffd7c5ba7087cdb8c2a060eb360017e850bf5d27b063eedffa9"); + let expected = hex!("dfcdb8ebb4499c60cd86a84a9b2a2adf"); + + let tag = retry_tag(&QuicVersion::V1, &orig_dst_cid, &packet); + assert_eq!(expected, tag) + } +} diff --git a/src/secret.rs b/src/secret.rs new file mode 100644 index 0000000..77537bd --- /dev/null +++ b/src/secret.rs @@ -0,0 +1,207 @@ +use crate::error::Result; +use crate::hkdf; +use crate::key::{HeaderKey, KeyPair, Keys, PacketKey}; +use crate::macros::bounded_array; +use crate::suite::CipherSuite; +use crate::version::QuicVersion; +use quinn_proto::{ConnectionId, Side}; + +const MAX_SECRET_LEN: usize = hkdf::DIGEST_BLOCK_LEN; + +bounded_array! { + /// A buffer that can fit the largest master secret. + pub(crate) struct Secret(MAX_SECRET_LEN) +} + +impl Secret { + /// Performs an in-place key update. + #[inline] + pub(crate) fn update(&mut self, version: QuicVersion, suite: &CipherSuite) -> Result<()> { + let out = &mut [0u8; Secret::MAX_LEN][..self.len()]; + suite + .hkdf + .expand_label(self.slice(), version.key_update_label(), out)?; + self.slice_mut().copy_from_slice(out); + Ok(()) + } + + #[inline] + pub(crate) fn header_key( + &self, + version: QuicVersion, + suite: &'static CipherSuite, + ) -> Result { + HeaderKey::new(version, suite, self) + } + + #[inline] + pub(crate) fn packet_key( + &self, + version: QuicVersion, + suite: &'static CipherSuite, + ) -> Result { + PacketKey::new(version, suite, self) + } +} + +/// A secret pair for reading (decryption) and writing (encryption). +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) struct Secrets { + pub(crate) version: QuicVersion, + pub(crate) suite: &'static CipherSuite, + pub(crate) local: Secret, + pub(crate) remote: Secret, +} + +impl Secrets { + /// Creates the Quic initial secrets. + /// See . + #[inline] + pub(crate) fn initial( + version: QuicVersion, + dst_cid: &ConnectionId, + side: Side, + ) -> Result { + // Initial secrets always use AES-128-GCM and SHA256. + let suite = CipherSuite::aes128_gcm_sha256(); + + // Generate the initial secret. + let salt = version.initial_salt(); + let mut initial_secret = [0u8; Secret::MAX_LEN]; + let initial_secret_len = suite.hkdf.extract(salt, dst_cid, &mut initial_secret)?; + let initial_secret = &initial_secret[..initial_secret_len]; + + // Use the appropriate secret labels for "this" side of the connection. + const CLIENT_LABEL: &[u8] = b"client in"; + const SERVER_LABEL: &[u8] = b"server in"; + let (local_label, remote_label) = match side { + Side::Client => (CLIENT_LABEL, SERVER_LABEL), + Side::Server => (SERVER_LABEL, CLIENT_LABEL), + }; + + let len = suite.hkdf.digest_size(); + let mut local = Secret::with_len(len); + suite + .hkdf + .expand_label(initial_secret, local_label, local.slice_mut())?; + + let mut remote = Secret::with_len(len); + suite + .hkdf + .expand_label(initial_secret, remote_label, remote.slice_mut())?; + + Ok(Secrets { + version, + suite, + local, + remote, + }) + } + + #[inline] + pub(crate) fn keys(&self) -> Result { + Ok(Keys { + header: self.header_keys()?, + packet: self.packet_keys()?, + }) + } + + #[inline] + pub(crate) fn header_keys(&self) -> Result> { + Ok(KeyPair { + local: self.local.header_key(self.version, self.suite)?, + remote: self.remote.header_key(self.version, self.suite)?, + }) + } + + #[inline] + pub(crate) fn packet_keys(&self) -> Result> { + Ok(KeyPair { + local: self.local.packet_key(self.version, self.suite)?, + remote: self.remote.packet_key(self.version, self.suite)?, + }) + } + + #[inline] + pub(crate) fn update(&mut self) -> Result<()> { + // Update the secrets. + self.local.update(self.version, self.suite)?; + self.remote.update(self.version, self.suite)?; + Ok(()) + } + + #[inline] + pub(crate) fn next_packet_keys(&mut self) -> Result> { + // Get the current keys. + let keys = self.packet_keys()?; + + // Update the secrets. + self.update()?; + + Ok(keys) + } +} + +pub(crate) struct SecretsBuilder { + pub(crate) version: QuicVersion, + pub(crate) suite: Option<&'static CipherSuite>, + pub(crate) local_secret: Option, + pub(crate) remote_secret: Option, +} + +impl SecretsBuilder { + pub(crate) fn new(version: QuicVersion) -> Self { + Self { + version, + suite: None, + local_secret: None, + remote_secret: None, + } + } + + pub(crate) fn set_suite(&mut self, suite: &'static CipherSuite) { + if let Some(prev) = self.suite { + // Make sure it doesn't change once set. + assert_eq!(prev, suite); + return; + } + + self.suite = Some(suite) + } + + pub(crate) fn set_remote_secret(&mut self, secret: Secret) { + if let Some(prev) = &self.remote_secret { + // Make sure it doesn't change once set. + assert_eq!(*prev, secret); + return; + } + + self.remote_secret = Some(secret) + } + + pub(crate) fn set_local_secret(&mut self, secret: Secret) { + if let Some(prev) = &self.local_secret { + // Make sure it doesn't change once set. + assert_eq!(*prev, secret); + return; + } + + self.local_secret = Some(secret) + } + + pub(crate) fn build(&self) -> Option { + if let Some(suite) = self.suite { + if let Some(local) = self.local_secret { + if let Some(remote) = self.remote_secret { + return Some(Secrets { + version: self.version, + suite, + local, + remote, + }); + } + } + } + None + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..0138c49 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,329 @@ +use crate::alpn::AlpnProtocols; +use crate::bffi_ext::QuicSsl; +use crate::error::{map_result, Result}; +use crate::secret::Secrets; +use crate::session_state::{SessionState, QUIC_METHOD}; +use crate::version::QuicVersion; +use crate::{retry, KeyLog, NoKeyLog, QuicSslContext}; +use boring::ssl::{Ssl, SslContext, SslContextBuilder, SslMethod, SslVersion}; +use boring_sys as bffi; +use bytes::{Bytes, BytesMut}; +use foreign_types_shared::ForeignType; +use once_cell::sync::Lazy; +use quinn_proto::{ + crypto, transport_parameters::TransportParameters, ConnectionId, Side, TransportError, +}; +use std::any::Any; +use std::ffi::{c_int, c_uint, c_void}; +use std::result::Result as StdResult; +use std::slice; +use std::sync::Arc; + +/// Configuration for a server-side QUIC. Wraps around a BoringSSL [SslContext]. +pub struct Config { + ctx: SslContext, + alpn_protocols: AlpnProtocols, + key_log: Option>, +} + +impl Config { + pub fn new() -> Result { + let mut builder = SslContextBuilder::new(SslMethod::tls())?; + + // QUIC requires TLS 1.3. + builder.set_min_proto_version(Some(SslVersion::TLS1_3))?; + builder.set_max_proto_version(Some(SslVersion::TLS1_3))?; + + builder.set_default_verify_paths()?; + + // We build the context early, since we are not allowed to further mutate the context + // in start_session. + let mut ctx = builder.build(); + + // Disable verification of the client by default. + ctx.verify_peer(false); + + // By default, enable early data (used for 0-RTT). + ctx.enable_early_data(true); + + // Configure default ALPN protocols accepted by the server.QUIC requires ALPN be + // configured (see https://www.rfc-editor.org/rfc/rfc9001.html#section-8.1). + ctx.set_alpn_select_cb(Some(Session::alpn_select_callback)); + + // Set the callback for receipt of the Server Name Indication (SNI) extension. + ctx.set_server_name_cb(Some(Session::server_name_callback)); + + // Set callbacks for the SessionState. + ctx.set_quic_method(&QUIC_METHOD)?; + ctx.set_info_callback(Some(SessionState::info_callback)); + ctx.set_keylog_callback(Some(SessionState::keylog_callback)); + + ctx.set_options(bffi::SSL_OP_CIPHER_SERVER_PREFERENCE as u32); + + Ok(Self { + ctx, + alpn_protocols: AlpnProtocols::default(), + key_log: None, + }) + } + + /// Returns the underlying [SslContext] backing all created sessions. + pub fn ctx(&self) -> &SslContext { + &self.ctx + } + + /// Returns the underlying [SslContext] backing all created sessions. Wherever possible use + /// the provided methods to modify settings rather than accessing this directly. + /// + /// Care should be taken to avoid overriding required behavior. In particular, this + /// configuration will set callbacks for QUIC events, alpn selection, server name, + /// as well as info and key logging. + pub fn ctx_mut(&mut self) -> &mut SslContext { + &mut self.ctx + } + + /// Sets whether or not the peer certificate should be verified. If `true`, any error + /// during verification will be fatal. If not called, verification of the client is + /// disabled by default. + pub fn verify_peer(&mut self, verify: bool) { + self.ctx.verify_peer(verify) + } + + /// Sets the ALPN protocols that will be accepted by the server. QUIC requires that + /// ALPN be used (see ). + /// + /// If this method is not called, the server will default to accepting "h3". + pub fn set_alpn(&mut self, alpn_protocols: &[Vec]) -> Result<()> { + self.alpn_protocols = alpn_protocols.into(); + Ok(()) + } + + /// Sets the key logger. + pub fn set_key_log(&mut self, key_log: Option>) { + self.key_log = key_log; + } +} + +impl crypto::ServerConfig for Config { + fn initial_keys( + &self, + version: u32, + dcid: &ConnectionId, + side: Side, + ) -> StdResult { + let version = QuicVersion::parse(version)?; + let secrets = Secrets::initial(version, dcid, side).unwrap(); + Ok(secrets.keys().unwrap().as_crypto().unwrap()) + } + + fn retry_tag(&self, version: u32, orig_dst_cid: &ConnectionId, packet: &[u8]) -> [u8; 16] { + let version = QuicVersion::parse(version).unwrap(); + retry::retry_tag(&version, orig_dst_cid, packet) + } + + fn start_session( + self: Arc, + version: u32, + params: &TransportParameters, + ) -> Box { + let version = QuicVersion::parse(version).unwrap(); + Session::new(self, version, params).unwrap() + } +} + +static SESSION_INDEX: Lazy = Lazy::new(|| unsafe { + bffi::SSL_get_ex_new_index(0, std::ptr::null_mut(), std::ptr::null_mut(), None, None) +}); + +/// The [crypto::Session] implementation for BoringSSL. +struct Session { + state: Box, + alpn: AlpnProtocols, + handshake_data_available: bool, + handshake_data_sent: bool, +} + +impl Session { + fn new( + cfg: Arc, + version: QuicVersion, + params: &TransportParameters, + ) -> Result> { + let mut ssl = Ssl::new(&cfg.ctx).unwrap(); + + // Configure the TLS extension based on the QUIC version used. + ssl.set_quic_use_legacy_codepoint(version.uses_legacy_extension()); + + // Configure the SSL to be a server. + ssl.set_accept_state(); + + // Set the transport parameters. + ssl.set_quic_transport_params(&encode_params(params)) + .unwrap(); + + // Need to se + ssl.set_quic_early_data_context(b"quinn-boring").unwrap(); + + let mut session = Box::new(Self { + state: SessionState::new( + ssl, + Side::Server, + version, + cfg.key_log + .as_ref() + .map_or(Arc::new(NoKeyLog), |key_log| key_log.clone()), + )?, + alpn: cfg.alpn_protocols.clone(), + handshake_data_available: false, + handshake_data_sent: false, + }); + + // Register the instance in SSL ex_data. This allows the static callbacks to + // reference the instance. + unsafe { + map_result(bffi::SSL_set_ex_data( + session.state.ssl.as_ptr(), + *SESSION_INDEX, + &mut *session as *mut Self as *mut _, + ))?; + } + + Ok(session) + } + + /// Server-side only callback from BoringSSL to select the ALPN protocol. + #[inline] + fn on_alpn_select<'a>(&mut self, offered: &'a [u8]) -> Result<&'a [u8]> { + // Indicate that we now have handshake data available. + self.handshake_data_available = true; + + self.alpn.select(offered) + } + + /// Server-side only callback from BoringSSL indicating that the Server Name Indication (SNI) + /// extension in the client hello was successfully parsed. + #[inline] + fn on_server_name(&mut self, _: *mut c_int) -> c_int { + // Indicate that we now have handshake data available. + self.handshake_data_available = true; + + // SSL_TLSEXT_ERR_OK causes the server_name extension to be acked in + // ServerHello. + bffi::SSL_TLSEXT_ERR_OK + } +} + +// Raw callbacks from BoringSSL +impl Session { + #[inline] + fn get_instance(ssl: *const bffi::SSL) -> &'static mut Session { + unsafe { + let data = bffi::SSL_get_ex_data(ssl, *SESSION_INDEX); + if data.is_null() { + panic!("BUG: Session instance missing") + } + &mut *(data as *mut Session) + } + } + + extern "C" fn alpn_select_callback( + ssl: *mut bffi::SSL, + out: *mut *const u8, + out_len: *mut u8, + in_: *const u8, + in_len: c_uint, + _: *mut c_void, + ) -> c_int { + let inst = Self::get_instance(ssl); + + unsafe { + let protos = slice::from_raw_parts(in_, in_len as _); + match inst.on_alpn_select(protos) { + Ok(proto) => { + *out = proto.as_ptr() as _; + *out_len = proto.len() as _; + bffi::SSL_TLSEXT_ERR_OK + } + Err(_) => bffi::SSL_TLSEXT_ERR_ALERT_FATAL, + } + } + } + + extern "C" fn server_name_callback( + ssl: *mut bffi::SSL, + out_alert: *mut c_int, + _: *mut c_void, + ) -> c_int { + let inst = Self::get_instance(ssl); + inst.on_server_name(out_alert) + } +} + +impl crypto::Session for Session { + fn initial_keys(&self, dcid: &ConnectionId, side: Side) -> crypto::Keys { + self.state.initial_keys(dcid, side) + } + + fn handshake_data(&self) -> Option> { + self.state.handshake_data() + } + + fn peer_identity(&self) -> Option> { + self.state.peer_identity() + } + + fn early_crypto(&self) -> Option<(Box, Box)> { + self.state.early_crypto() + } + + fn early_data_accepted(&self) -> Option { + None + } + + fn is_handshaking(&self) -> bool { + self.state.is_handshaking() + } + + fn read_handshake(&mut self, plaintext: &[u8]) -> StdResult { + self.state.read_handshake(plaintext)?; + + // Only indicate that handshake data is available once. + if !self.handshake_data_sent && self.handshake_data_available { + self.handshake_data_sent = true; + return Ok(true); + } + + Ok(false) + } + + fn transport_parameters(&self) -> StdResult, TransportError> { + self.state.transport_parameters() + } + + fn write_handshake(&mut self, buf: &mut Vec) -> Option { + self.state.write_handshake(buf) + } + + fn next_1rtt_keys(&mut self) -> Option>> { + self.state.next_1rtt_keys() + } + + fn is_valid_retry(&self, orig_dst_cid: &ConnectionId, header: &[u8], payload: &[u8]) -> bool { + self.state.is_valid_retry(orig_dst_cid, header, payload) + } + + fn export_keying_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> StdResult<(), crypto::ExportKeyingMaterialError> { + self.state.export_keying_material(output, label, context) + } +} + +fn encode_params(params: &TransportParameters) -> Bytes { + let mut out = BytesMut::with_capacity(128); + params.write(&mut out); + out.freeze() +} diff --git a/src/session_cache.rs b/src/session_cache.rs new file mode 100644 index 0000000..1b8819f --- /dev/null +++ b/src/session_cache.rs @@ -0,0 +1,180 @@ +use crate::error::Result; +use crate::{Error, QuicSslSession}; +use boring::ssl::{SslContextRef, SslSession}; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use lru::LruCache; +use quinn_proto::{transport_parameters::TransportParameters, Side}; +use std::num::NonZeroUsize; +use std::sync::Mutex; + +/// A client-side Session cache for the BoringSSL crypto provider. +pub trait SessionCache: Send + Sync { + /// Adds the given value to the session cache. + fn put(&self, key: Bytes, value: Bytes); + + /// Returns the cached session, if it exists. + fn get(&self, key: Bytes) -> Option; + + /// Removes the cached session, if it exists. + fn remove(&self, key: Bytes); + + /// Removes all entries from the cache. + fn clear(&self); +} + +/// A utility for combining an [SslSession] and server [TransportParameters] as a +/// [SessionCache] entry. +pub struct Entry { + pub session: SslSession, + pub params: TransportParameters, +} + +impl Entry { + /// Encodes this [Entry] into a [SessionCache] value. + pub fn encode(&self) -> Result { + let mut out = BytesMut::with_capacity(2048); + + // Split the buffer in two: the length prefix buffer and the encoded session buffer. + // This will be O(1) as both will refer to the same underlying buffer. + let mut encoded = out.split_off(8); + + // Store the session in the second buffer. + self.session.encode(&mut encoded)?; + + // Go back and write the length to the first buffer. + out.put_u64(encoded.len() as u64); + + // Unsplit to merge the two buffers back together. This will be O(1) since + // the buffers are already contiguous in memory. + out.unsplit(encoded); + + // Now add the transport parameters. + out.reserve(128); + let mut encoded = out.split_off(out.len() + 8); + self.params.write(&mut encoded); + out.put_u64(encoded.len() as u64); + out.unsplit(encoded); + + Ok(out.freeze()) + } + + /// Decodes a [SessionCache] value into an [Entry]. + pub fn decode(ctx: &SslContextRef, mut encoded: Bytes) -> Result { + // Decode the session. + let len = encoded.get_u64() as usize; + let mut encoded_session = encoded.split_to(len); + let session = SslSession::decode(ctx, &mut encoded_session)?; + + // Decode the transport parameters. + let len = encoded.get_u64() as usize; + let mut encoded_params = encoded.split_to(len); + let params = TransportParameters::read(Side::Client, &mut encoded_params).map_err(|e| { + Error::invalid_input(format!( + "failed parsing cached transport parameters: {:?}", + e + )) + })?; + + Ok(Self { session, params }) + } +} + +/// A [SessionCache] implementation that will never cache anything. Requires no storage. +pub struct NoSessionCache; + +impl SessionCache for NoSessionCache { + fn put(&self, _: Bytes, _: Bytes) {} + + fn get(&self, _: Bytes) -> Option { + None + } + + fn remove(&self, _: Bytes) {} + + fn clear(&self) {} +} + +pub struct SimpleCache { + cache: Mutex>, +} + +impl SimpleCache { + pub fn new(num_entries: usize) -> Self { + SimpleCache { + cache: Mutex::new(LruCache::new(NonZeroUsize::new(num_entries).unwrap())), + } + } +} + +impl SessionCache for SimpleCache { + fn put(&self, key: Bytes, value: Bytes) { + let _ = self.cache.lock().unwrap().put(key, value); + } + + fn get(&self, key: Bytes) -> Option { + self.cache.lock().unwrap().get(&key).cloned() + } + + fn remove(&self, key: Bytes) { + let _ = self.cache.lock().unwrap().pop(&key); + } + + fn clear(&self) { + self.cache.lock().unwrap().clear() + } +} + +#[cfg(test)] +mod tests { + use crate::session_cache::Entry; + use crate::ClientConfig; + use bytes::{BufMut, BytesMut}; + use hex_literal::hex; + + #[test] + fn entry_encoding() { + let encoded = { + // Captured output from integration tests. + let encoded_session = hex!("30820412020101020203040402130104206ed381170bbf75" + "7901238adcd0a96ee46d1642775001abf602f69484510419d904201f66fb5b215a4f3a5fb5251c9a9a" + "17cad88582361f0042faf2a000eb303f42e4a106020463e53fbea205020302a300a382015630820152" + "3081f9a003020102020900b06e4d934b5c5d0d300a06082a8648ce3d0403023021311f301d06035504" + "030c16726367656e2073656c66207369676e656420636572743020170d373530313031303030303030" + "5a180f34303936303130313030303030305a3021311f301d06035504030c16726367656e2073656c66" + "207369676e656420636572743059301306072a8648ce3d020106082a8648ce3d030107034200047582" + "ef451a59ecae9cb170d4959664eb5631696e553f20df7db5f7cb59f550d67b795738145cf4bcde0e45" + "4d0f3bd8d6a2510c75cc66ccaedf4a1340d6166c4ea318301630140603551d11040d300b82096c6f63" + "616c686f7374300a06082a8648ce3d0403020348003045022100a46020ca34d0e8b7d79e4894d5c97f" + "0eb72962a42cce0d59b8e83817db2216e302205993ee4d874d6d94e32c001354d5ad17959561ac9856" + "5fc58abcb1860d2ca3f8a4020400aa81b30481b05c0ece9d9fe026ff4d507ca869cac8734184a0b12e" + "18c7551a8612b0de5e409a6f2c5f1fde44ca8ebf6db38e663584a0f9196fa420f38490edf53f553ff9" + "cdeb010fb86bebe050289a9e2af41aa6046b5f82d835921f6cca8777085d5dc6c662201331d6ac40bb" + "65f11436cf18f0da48e0049ea5aab7e43fc1ba8bb784cbb6248ed26aae2a3fb3d487a040660b7057b3" + "a16dd517ce22f3d4d80f2d994520cf1016cf79dad8fb763319b61b9d7abd8278b20302011db3820171" + "3082016d30820112a0030201020208383312a7a721d5a4300a06082a8648ce3d0403023021311f301d" + "06035504030c16726367656e2073656c66207369676e656420636572743020170d3735303130313030" + "303030305a180f34303936303130313030303030305a3021311f301d06035504030c16726367656e20" + "73656c66207369676e656420636572743059301306072a8648ce3d020106082a8648ce3d0301070342" + "000465ed18244bdfe0b42219d0277c4984a57723a5391d41b658ec6eb1c20d3fb4d0239835d163c158" + "88a39e3791b738b24a1b47b35a3c16f9e06a2773495bcc43bca3323030301d0603551d0e04160414a4" + "d521a7a7123338ed83272d04d60e50f4c39c1c300f0603551d130101ff040530030101ff300a06082a" + "8648ce3d040302034900304602210082207a475582b6a2a48ffecea8dfe3aea6b84a19919a392ae779" + "282bf6074e64022100aa5e1fd03607db29ff144d61815f6730f60d9fe4f227b2f093b93f3acb5cefa4" + "b506040425b2131eb603010100b70402020403b9050203093a80ba050403666f6fbb030101ff"); + let encoded_params = hex!("01026710030245c80408ffffffffffffffff0504801312d00604801312d0" + "0704801312d008024064090240640e010540b60020048000ffff0f0880145d492e958b6f6ab200"); + let mut out = BytesMut::new(); + out.put_u64(encoded_session.len() as u64); + out.extend_from_slice(&encoded_session); + out.put_u64(encoded_params.len() as u64); + out.extend_from_slice(&encoded_params); + out.freeze() + }; + + let cfg = ClientConfig::new().unwrap(); + let ctx = cfg.ctx().as_ref(); + let decoded = Entry::decode(ctx, encoded.clone()).unwrap(); + let re_encoded = decoded.encode().unwrap(); + assert_eq!(&encoded, &re_encoded); + } +} diff --git a/src/session_state.rs b/src/session_state.rs new file mode 100644 index 0000000..860d39f --- /dev/null +++ b/src/session_state.rs @@ -0,0 +1,621 @@ +use crate::alert::Alert; +use crate::error::{map_cb_result, map_result, Result}; +use crate::secret::{Secret, Secrets, SecretsBuilder}; +use crate::suite::CipherSuite; +use crate::{ + retry, Error, HandshakeData, KeyLog, KeyLogLabel, Level, QuicSsl, QuicVersion, SslError, +}; +use boring::error::ErrorStack; +use boring::ssl::{NameType, Ssl}; +use boring_sys as bffi; +use bytes::{Buf, BytesMut}; +use foreign_types_shared::ForeignType; +use once_cell::sync::Lazy; +use quinn_proto::{ + crypto, transport_parameters::TransportParameters, ConnectionId, Side, TransportError, +}; +use std::any::Any; +use std::ffi::{c_char, c_int, CStr}; +use std::io::Cursor; +use std::result::Result as StdResult; +use std::slice; +use std::sync::Arc; +use tracing::{error, trace, warn}; + +pub(crate) static QUIC_METHOD: bffi::SSL_QUIC_METHOD = bffi::SSL_QUIC_METHOD { + set_read_secret: Some(SessionState::set_read_secret_callback), + set_write_secret: Some(SessionState::set_write_secret_callback), + add_handshake_data: Some(SessionState::add_handshake_data_callback), + flush_flight: Some(SessionState::flush_flight_callback), + send_alert: Some(SessionState::send_alert_callback), +}; + +static SESSION_INDEX: Lazy = Lazy::new(|| unsafe { + bffi::SSL_get_ex_new_index(0, std::ptr::null_mut(), std::ptr::null_mut(), None, None) +}); + +pub(crate) struct SessionState { + pub(crate) ssl: Ssl, + pub(crate) version: QuicVersion, + + /// Indicates that early data was rejected in the last call to [Self::read_handshake]. + pub(crate) early_data_rejected: bool, + + side: Side, + key_log: Arc, + alert: Option, + next_secrets: Option, + keys_updated: bool, + read_level: Level, + write_level: Level, + levels: [LevelState; Level::NUM_LEVELS], + handshaking: bool, +} + +impl SessionState { + pub(crate) fn new( + ssl: Ssl, + side: Side, + version: QuicVersion, + key_log: Arc, + ) -> Result> { + let levels = [ + LevelState::new(version, Level::Initial, &ssl), + LevelState::new(version, Level::EarlyData, &ssl), + LevelState::new(version, Level::Handshake, &ssl), + LevelState::new(version, Level::Application, &ssl), + ]; + + let mut state = Box::new(Self { + ssl, + version, + side, + key_log, + alert: None, + next_secrets: None, + keys_updated: false, + read_level: Level::Initial, + write_level: Level::Initial, + levels, + early_data_rejected: false, + handshaking: true, + }); + + // Registers this instance as ex data on the underlying Ssl in order to support + // BoringSSL callbacks to this instance. + unsafe { + map_result(bffi::SSL_set_ex_data( + state.ssl.as_ptr(), + *SESSION_INDEX, + &mut *state as *mut Self as *mut _, + ))?; + } + + Ok(state) + } + + #[inline] + fn level_state(&self, level: Level) -> &LevelState { + &self.levels[level as usize] + } + + #[inline] + fn level_state_mut(&mut self, level: Level) -> &mut LevelState { + &mut self.levels[level as usize] + } + + #[inline] + pub(crate) fn is_handshaking(&self) -> bool { + self.handshaking + } + + #[inline] + pub(crate) fn handshake_data(&self) -> Option> { + let sni_name = if self.side.is_server() { + self.ssl + .servername(NameType::HOST_NAME) + .map(|server_name| server_name.to_string()) + } else { + // Server name does not apply to the client. + None + }; + + let alpn_protocol = self.ssl.selected_alpn_protocol().map(Vec::from); + + if sni_name.is_none() && alpn_protocol.is_none() { + None + } else { + Some(Box::new(HandshakeData { + protocol: alpn_protocol, + server_name: sni_name, + })) + } + } + + #[inline] + pub(crate) fn next_1rtt_keys(&mut self) -> Option>> { + self.next_secrets + .as_mut() + .map(|secrets| secrets.next_packet_keys().unwrap().as_crypto().unwrap()) + } + + #[inline] + pub(crate) fn transport_parameters( + &self, + ) -> StdResult, TransportError> { + match self.ssl.get_peer_quic_transport_params() { + Some(params) => { + let params = TransportParameters::read(self.side, &mut Cursor::new(params)) + .map_err(|e| TransportError { + code: Alert::handshake_failure().into(), + frame: None, + reason: format!("failed parsing transport params: {:?}", e), + })?; + Ok(Some(params)) + } + None => Ok(None), + } + } + + #[inline] + pub(crate) fn read_handshake(&mut self, plaintext: &[u8]) -> StdResult<(), TransportError> { + let ssl_err = self.ssl.provide_quic_data(self.read_level, plaintext); + self.check_alert()?; + self.check_ssl_error(ssl_err)?; + + self.advance_handshake() + } + + #[inline] + pub(crate) fn write_handshake(&mut self, buf: &mut Vec) -> Option { + // Write all available data at the current write level. + let write_state = self.level_state_mut(self.write_level); + if write_state.write_buffer.has_remaining() { + buf.extend_from_slice(&write_state.write_buffer); + write_state.write_buffer.clear(); + } + + // Advance to the next write level. + let ssl_engine_write_level = self.ssl.quic_write_level(); + let next_write_level = self.write_level.next(); + if next_write_level != self.write_level && next_write_level <= ssl_engine_write_level { + self.write_level = next_write_level; + + // Indicate that we're updating the keys. + self.keys_updated = true; + } + + let out = if self.keys_updated { + self.keys_updated = false; + + if self.next_secrets.is_some() { + // Once we've returned the application secrets, stop sending key updates. + None + } else { + // Determine if we're transitioning to the application-level keys. + let is_app = self.write_level == Level::Application; + + // Build the secrets. + let secrets = self + .level_state(self.write_level) + .builder + .build() + .unwrap_or_else(|| { + panic!("failed building secrets for level {:?}", self.write_level) + }); + + if is_app { + // We've transitioned to the application level, we need to set the + // next (i.e. application) secrets for use from next_1rtt_keys. + + // Copy the secrets and advance them to the next application secrets. + let mut next_app_secrets = secrets; + next_app_secrets.update().unwrap(); + + self.next_secrets = Some(next_app_secrets); + } + + Some(secrets.keys().unwrap()) + } + } else { + None + }; + + out.map(|keys| keys.as_crypto().unwrap()) + } + + #[inline] + pub(crate) fn is_valid_retry( + &self, + orig_dst_cid: &ConnectionId, + header: &[u8], + payload: &[u8], + ) -> bool { + retry::is_valid_retry(&self.version, orig_dst_cid, header, payload) + } + + #[inline] + pub(crate) fn peer_identity(&self) -> Option> { + todo!() + } + + #[inline] + pub(crate) fn early_crypto( + &self, + ) -> Option<(Box, Box)> { + let builder = &self.level_state(Level::EarlyData).builder; + let version = builder.version; + let suite = builder.suite?; + let early_secret = match self.side { + Side::Client => builder.local_secret?, + Side::Server => builder.remote_secret?, + }; + let header_key = early_secret + .header_key(version, suite) + .unwrap() + .as_crypto() + .unwrap(); + let packet_key = Box::new(early_secret.packet_key(version, suite).unwrap()); + + Some((header_key, packet_key)) + } + + #[inline] + pub(crate) fn initial_keys(&self, dcid: &ConnectionId, side: Side) -> crypto::Keys { + let secrets = Secrets::initial(self.version, dcid, side).unwrap(); + secrets.keys().unwrap().as_crypto().unwrap() + } + + #[inline] + pub(crate) fn export_keying_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> StdResult<(), crypto::ExportKeyingMaterialError> { + self.ssl + .export_keyring_material(output, label, context) + .map_err(|_| crypto::ExportKeyingMaterialError {}) + } + + #[inline] + pub(crate) fn advance_handshake(&mut self) -> StdResult<(), TransportError> { + self.early_data_rejected = false; + + if self.handshaking { + let rc = self.ssl.do_handshake(); + + // Update the state of the handshake. + self.handshaking = self.ssl.is_handshaking(); + + self.check_alert()?; + self.check_ssl_error(rc)?; + } + + if !self.handshaking { + let ssl_err = self.ssl.process_post_handshake(); + self.check_alert()?; + return self.check_ssl_error(ssl_err); + } + Ok(()) + } + + #[inline] + pub(crate) fn check_alert(&self) -> StdResult<(), TransportError> { + if let Some(alert) = &self.alert { + return Err(alert.clone()); + } + Ok(()) + } + + #[inline] + pub(crate) fn check_ssl_error(&mut self, ssl_err: SslError) -> StdResult<(), TransportError> { + match ssl_err.value() { + bffi::SSL_ERROR_NONE => Ok(()), + bffi::SSL_ERROR_WANT_READ + | bffi::SSL_ERROR_WANT_WRITE + | bffi::SSL_ERROR_PENDING_SESSION + | bffi::SSL_ERROR_PENDING_CERTIFICATE + | bffi::SSL_ERROR_PENDING_TICKET + | bffi::SSL_ERROR_WANT_X509_LOOKUP + | bffi::SSL_ERROR_WANT_PRIVATE_KEY_OPERATION + | bffi::SSL_ERROR_WANT_CERTIFICATE_VERIFY => { + // Not an error - retry when we get more data from the peer. + trace!("SSL:{}", ssl_err.get_description()); + Ok(()) + } + bffi::SSL_ERROR_EARLY_DATA_REJECTED => { + // Reset the state to allow retry with 1-RTT. + self.ssl.reset_early_rejected_data(); + + // Indicate that the early data has been rejected for the current handshake. + self.early_data_rejected = true; + Ok(()) + } + _ => { + // Everything else is fatal. + let reason = if ssl_err.value() == bffi::SSL_ERROR_SSL { + // Error occurred within the SSL library. Get details from the ErrorStack. + format!("{}: {:?}", ssl_err, ErrorStack::get()) + } else { + format!("{}", ssl_err) + }; + + let mut err: TransportError = Alert::handshake_failure().into(); + err.reason = reason; + Err(err) + } + } + } +} + +// BoringSSL event handlers. +impl SessionState { + /// Callback from BoringSSL that configures the read secret and cipher suite for the given + /// encryption level. If an error is returned, the handshake is terminated with an error. + /// This function will be called at most once per encryption level. + #[inline] + fn on_set_read_secret( + &mut self, + level: Level, + suite: &'static CipherSuite, + secret: Secret, + ) -> Result<()> { + // Store the secret. + let builder = &mut self.level_state_mut(level).builder; + builder.set_suite(suite); + builder.set_remote_secret(secret); + + // Advance the currently active read level. + self.read_level = level; + + // Indicate that the next call to write_handshake should generate new keys. + self.keys_updated = true; + Ok(()) + } + + /// Callback from BoringSSL that configures the write secret and cipher suite for the given + /// encryption level. If an error is returned, the handshake is terminated with an error. + /// This function will be called at most once per encryption level. + #[inline] + fn on_set_write_secret( + &mut self, + level: Level, + suite: &'static CipherSuite, + secret: Secret, + ) -> Result<()> { + // Store the secret. + let builder = &mut self.level_state_mut(level).builder; + builder.set_suite(suite); + builder.set_local_secret(secret); + Ok(()) + } + + /// Callback from BoringSSL that adds handshake data to the current flight at the given + /// encryption level. If an error is returned, the handshake is terminated with an error. + #[inline] + fn on_add_handshake_data(&mut self, level: Level, data: &[u8]) -> Result<()> { + if level < self.write_level { + return Err(Error::other(format!( + "add_handshake_data for previous write level {:?}", + level + ))); + } + + // Make sure we don't exceed the buffer capacity for the level. + let state = self.level_state_mut(level); + if state.write_buffer.len() + data.len() > state.write_buffer.capacity() { + return Err(Error::other(format!( + "add_handshake_data exceeded buffer capacity for level {:?}", + level + ))); + } + + // Add the message to the level. + state.write_buffer.extend_from_slice(data); + Ok(()) + } + + /// Callback from BoringSSL called when the current flight is complete and should be + /// written to the transport. Note a flight may contain data at several + /// encryption levels. + #[inline] + fn on_flush_flight(&mut self) -> Result<()> { + Ok(()) + } + + /// Callback from BoringSSL that sends a fatal alert at the specified encryption level. + #[inline] + fn on_send_alert(&mut self, _: Level, alert: Alert) -> Result<()> { + self.alert = Some(alert.into()); + Ok(()) + } + + /// Callback from BoringSSL to handle (i.e. log) info events. + fn on_info(&self, type_: c_int, value: c_int) { + if type_ & bffi::SSL_CB_LOOP > 0 { + trace!("SSL:ACCEPT_LOOP:{}", self.ssl.state_string()); + } else if type_ & bffi::SSL_CB_ALERT > 0 { + let prefix = if type_ & bffi::SSL_CB_READ > 0 { + "SSL:ALERT:READ:" + } else { + "SSL:ALERT:WRITE:" + }; + + if ((type_ & 0xF0) >> 8) == bffi::SSL3_AL_WARNING { + warn!("{}{}", prefix, self.ssl.state_string()); + } else { + error!("{}{}", prefix, self.ssl.state_string()); + } + } else if type_ & bffi::SSL_CB_EXIT > 0 { + if value == 1 { + trace!("SSL:ACCEPT_EXIT_OK:{}", self.ssl.state_string()); + } else { + // Not necessarily an actual error. It could just require additional + // data from the other side. + trace!("SSL:ACCEPT_EXIT_FAIL:{}", self.ssl.state_string()); + } + } else if type_ & bffi::SSL_CB_HANDSHAKE_START > 0 { + trace!("SSL:HANDSHAKE_START:{}", self.ssl.state_string()); + } else if type_ & bffi::SSL_CB_HANDSHAKE_DONE > 0 { + trace!("SSL:HANDSHAKE_DONE:{}", self.ssl.state_string()); + } else { + warn!( + "SSL:unknown event type {}:{}", + type_, + self.ssl.state_string() + ); + } + } + + fn on_keylog(&mut self, line: &str) { + // The log line is in the form: