Skip to content

Commit

Permalink
Introducing the Boring crypto provider.
Browse files Browse the repository at this point in the history
Also adding examples and basic documentation.
  • Loading branch information
nmittler committed Apr 3, 2023
1 parent 3621968 commit 14e2ffd
Show file tree
Hide file tree
Showing 27 changed files with 5,574 additions and 5 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable, beta, 1.59.0]
rust: [stable, beta]
exclude:
- os: macos-latest
rust: beta
- os: macos-latest
rust: 1.59.0
rust: 1.68.0
- os: windows-latest
rust: beta
- os: windows-latest
rust: 1.59.0
rust: 1.68.0

runs-on: ${{ matrix.os }}

Expand Down
35 changes: 34 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
# TODO
[![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*
45 changes: 45 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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.
145 changes: 145 additions & 0 deletions examples/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//! 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 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<String>,

/// Custom certificate authority to trust, in DER format
#[clap(parse(from_os_str), long = "ca")]
ca: Option<PathBuf>,

/// 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.add_trusted_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.add_trusted_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
}
Loading

0 comments on commit 14e2ffd

Please sign in to comment.