feat(boring): adapt boring2 for quinn (#87)

This commit is contained in:
0x676e67 2025-07-25 07:28:03 +08:00 committed by GitHub
parent 6bcc6af083
commit d9a1d9442e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 4680 additions and 0 deletions

View File

@ -101,21 +101,25 @@ jobs:
rust: stable rust: stable
os: ubuntu-latest os: ubuntu-latest
check_only: true check_only: true
extra_test_args: --workspace --exclude quinn-boring2
- thing: arm64-android - thing: arm64-android
target: aarch64-linux-android target: aarch64-linux-android
rust: stable rust: stable
os: ubuntu-latest os: ubuntu-latest
check_only: true check_only: true
extra_test_args: --workspace --exclude quinn-boring2
- thing: i686-android - thing: i686-android
target: i686-linux-android target: i686-linux-android
rust: stable rust: stable
os: ubuntu-latest os: ubuntu-latest
check_only: true check_only: true
extra_test_args: --workspace --exclude quinn-boring2
- thing: x86_64-android - thing: x86_64-android
target: x86_64-linux-android target: x86_64-linux-android
rust: stable rust: stable
os: ubuntu-latest os: ubuntu-latest
check_only: true check_only: true
extra_test_args: --workspace --exclude quinn-boring2
- thing: aarch64-ios - thing: aarch64-ios
target: aarch64-apple-ios target: aarch64-apple-ios
os: macos-latest os: macos-latest
@ -319,6 +323,8 @@ jobs:
sleep 10 sleep 10
echo "=== Publishing compio-boring2... ===" echo "=== Publishing compio-boring2... ==="
(cd compio-boring && cargo publish) (cd compio-boring && cargo publish)
echo "=== Publishing quinn-boring2... ==="
(cd quinn-boring && cargo publish)
- name: Upload binaries to GitHub Release - name: Upload binaries to GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2

View File

@ -4,6 +4,7 @@ members = [
"boring-sys", "boring-sys",
"tokio-boring", "tokio-boring",
"compio-boring", "compio-boring",
"quinn-boring",
] ]
resolver = "2" resolver = "2"

6
quinn-boring/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
/Cargo.lock
.idea
.DS_Store
.vscode

63
quinn-boring/Cargo.toml Normal file
View File

@ -0,0 +1,63 @@
[package]
name = "quinn-boring2"
version = { workspace = true }
authors = ["0x676e67 <gngppz@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = { workspace = true }
repository = { workspace = true }
description = "BoringSSL crypto provider for quinn"
keywords = ["quic"]
categories = ["network-programming", "asynchronous"]
rust-version = "1.85"
[badges]
maintenance = { status = "passively-maintained" }
[features]
default = ["runtime-tokio"]
runtime-tokio = ["quinn/runtime-tokio"]
# Enables experimental post-quantum crypto (https://blog.cloudflare.com/post-quantum-for-all/)
pq-experimental = ["boring/pq-experimental"]
[dependencies]
boring = { workspace = true }
boring-sys = { workspace = true }
bytes = "1"
foreign-types-shared = "0.3"
lru = "0.16"
quinn = { version = "0.11.8", default-features = false }
quinn-proto = { version = "0.11.12", default-features = false }
rand = "0.9.2"
tracing = "0.1"
[dev-dependencies]
anyhow = "1.0"
assert_matches = "1"
clap = { version = "4", features = ["derive"] }
directories-next = "2"
hex-literal = "0.4"
rcgen = "0.13"
rustls-pemfile = "2"
tokio = { version = "1", features = [
"rt",
"rt-multi-thread",
"time",
"macros",
"sync",
] }
tracing-subscriber = { version = "0.3", default-features = false, features = [
"fmt",
] }
url = "2"
h3 = "0.0.8"
h3-quinn = "0.0.10"
http = "1"
http-body = "1.0.1"
http-body-util = "0.1.3"
futures-core = "0.3"
[[example]]
name = "client"
required-features = ["runtime-tokio"]

201
quinn-boring/LICENSE-APACHE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

8
quinn-boring/LICENSE-MIT Normal file
View File

@ -0,0 +1,8 @@
Copyright (c) 2018 The quinn Developers
Copyright (c) 2025 0x676e67 <gngppz@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

7
quinn-boring/README.md Normal file
View File

@ -0,0 +1,7 @@
# quinn-boring2
A crypto provider for [quinn](https://github.com/quinn-rs/quinn) based on [BoringSSL](https://github.com/google/boringssl).
## Accolades
The project is based on a fork of [quinn-boring](https://github.com/quinn-rs/quinn-boring).

View File

@ -0,0 +1,202 @@
//! This example demonstrates an HTTP client that requests files from a server.
//!
//! Checkout the `README.md` for guidance.
use anyhow::{anyhow, Result};
use boring::x509::X509;
use bytes::BytesMut;
use bytes::{Buf, Bytes};
use clap::Parser;
use http::Uri;
use http_body::Body;
use http_body_util::BodyExt;
use quinn_boring2::QuicSslContext;
use std::{
fs,
net::{SocketAddr, ToSocketAddrs},
path::PathBuf,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Instant,
};
use tracing::Level;
use url::Url;
/// HTTP/0.9 over QUIC client
#[derive(Parser, Debug)]
#[clap(name = "client")]
struct Opt {
/// Log level e.g. trace, debug, info, warn, error
#[clap(long, default_value = "info")]
log: Level,
url: Url,
/// Override hostname used for certificate verification
#[clap(long)]
host: Option<String>,
/// Custom certificate authority to trust, in DER format
#[clap(long)]
ca: Option<PathBuf>,
/// Simulate NAT rebinding after connecting
#[clap(long)]
rebind: bool,
/// Address to bind on
#[clap(long, default_value = "[::]:0")]
bind: SocketAddr,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let options = Opt::parse();
let url = options.url;
let url_host = strip_ipv6_brackets(url.host_str().unwrap());
let remote = (url_host, url.port().unwrap_or(443))
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("couldn't resolve to an address"))?;
let mut client_crypto = quinn_boring2::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 {
client_crypto
.ctx_mut()
.cert_store_mut()
.set_default_paths()?;
}
let mut endpoint = quinn_boring2::helpers::client_endpoint(options.bind)?;
endpoint.set_default_client_config(quinn::ClientConfig::new(Arc::new(client_crypto)));
let start = Instant::now();
let rebind = options.rebind;
let host = options.host.as_deref().unwrap_or(url_host);
tracing::info!("connecting to {host} at {remote}");
let conn = endpoint
.connect(remote, host)?
.await
.map_err(|e| anyhow!("failed to connect: {}", e))?;
tracing::info!("connected at {:?}", start.elapsed());
if rebind {
let socket = std::net::UdpSocket::bind("[::]:0").unwrap();
let addr = socket.local_addr().unwrap();
tracing::info!("rebinding to {addr}");
endpoint.rebind(socket).expect("rebind failed");
}
let (mut h3_conn, mut tx) = h3::client::new(h3_quinn::Connection::new(conn)).await?;
let req = http::Request::get(Uri::try_from(url.as_str())?).body(())?;
let (mut send, mut recv) = tx.send_request(req).await?.split();
if let Err(e) = send.finish().await {
tracing::error!("failed to send request: {e}");
}
let resp = {
let resp = recv.recv_response().await?;
let resp_body = Incoming::new(recv, resp.headers())
.map_err(Into::<BoxError>::into)
.boxed();
resp.map(|_| resp_body)
};
tracing::info!("response: {:#?}", resp);
let body = BodyExt::collect(resp.into_body())
.await
.map(|buf| buf.to_bytes())
.map_err(|e| anyhow!("failed to collect response body: {e}"))?;
tracing::info!("response body: {}", String::from_utf8_lossy(&body));
h3_conn.shutdown(0).await?;
Ok(())
}
fn strip_ipv6_brackets(host: &str) -> &str {
// An ipv6 url looks like eg https://[::1]:4433/Cargo.toml, wherein the host [::1] is the
// ipv6 address ::1 wrapped in brackets, per RFC 2732. This strips those.
if host.starts_with('[') && host.ends_with(']') {
&host[1..host.len() - 1]
} else {
host
}
}
type BoxError = Box<dyn std::error::Error + Send + Sync>;
struct Incoming<S, B> {
inner: h3::client::RequestStream<S, B>,
content_length: Option<u64>,
}
impl<S, B> Incoming<S, B> {
fn new(stream: h3::client::RequestStream<S, B>, headers: &http::header::HeaderMap) -> Self {
Self {
inner: stream,
content_length: headers
.get(http::header::CONTENT_LENGTH)
.and_then(|h| h.to_str().ok())
.and_then(|v| v.parse().ok()),
}
}
}
impl<S, B> http_body::Body for Incoming<S, B>
where
S: h3::quic::RecvStream,
{
type Data = Bytes;
type Error = BoxError;
fn poll_frame(
mut self: Pin<&mut Self>,
cx: &mut Context,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
match futures_core::ready!(self.inner.poll_recv_data(cx)) {
Ok(Some(mut b)) => Poll::Ready(Some(Ok(http_body::Frame::data(
b.copy_to_bytes(b.remaining()),
)))),
Ok(None) => Poll::Ready(None),
Err(e) => Poll::Ready(Some(Err(e.into()))),
}
}
fn size_hint(&self) -> http_body::SizeHint {
if let Some(content_length) = self.content_length {
http_body::SizeHint::with_exact(content_length)
} else {
http_body::SizeHint::default()
}
}
}
pub async fn body_to_string<B>(
mut body: B,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>>
where
B: Body<Data = bytes::Bytes> + Unpin,
B::Error: std::error::Error + Send + Sync + 'static,
{
let mut buf = BytesMut::new();
while let Some(frame) = body.frame().await {
let frame = frame?;
if let Some(data) = frame.data_ref() {
buf.extend_from_slice(data);
}
}
Ok(String::from_utf8(buf.to_vec())?)
}

152
quinn-boring/src/aead.rs Normal file
View File

@ -0,0 +1,152 @@
use crate::error::{map_result, Error, Result};
use crate::key::{Key, Nonce, Tag};
use boring_sys as bffi;
use std::mem::MaybeUninit;
use std::sync::LazyLock;
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: LazyLock<Aead> = LazyLock::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: LazyLock<Aead> = LazyLock::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: LazyLock<Aead> = LazyLock::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<bffi::EVP_AEAD_CTX> {
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)
}
}

83
quinn-boring/src/alert.rs Normal file
View File

@ -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<Self, Self::Err> {
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<Alert> for TransportErrorCode {
fn from(alert: Alert) -> Self {
TransportErrorCode::crypto(alert.0)
}
}
impl From<Alert> for TransportError {
fn from(alert: Alert) -> Self {
TransportError {
code: alert.into(),
frame: None,
reason: alert.get_description().to_string(),
}
}
}

73
quinn-boring/src/alpn.rs Normal file
View File

@ -0,0 +1,73 @@
use crate::error::{Error, Result};
#[derive(Clone, Debug)]
pub(crate) struct AlpnProtocol(Vec<u8>);
impl AlpnProtocol {
#[inline]
pub(crate) fn encode(&self, encoded: &mut Vec<u8>) {
encoded.push(self.0.len() as u8);
encoded.extend_from_slice(&self.0);
}
}
impl From<Vec<u8>> for AlpnProtocol {
fn from(value: Vec<u8>) -> Self {
Self(value)
}
}
#[derive(Clone, Debug)]
pub(crate) struct AlpnProtocols(Vec<AlpnProtocol>);
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<u8> {
let mut out: Vec<u8> = 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<u8>]> for AlpnProtocols {
fn from(protos: &[Vec<u8>]) -> Self {
let mut out = Vec::with_capacity(protos.len());
for proto in protos {
out.push(AlpnProtocol(proto.clone()))
}
Self(out)
}
}
impl From<&Vec<Vec<u8>>> for AlpnProtocols {
fn from(protos: &Vec<Vec<u8>>) -> Self {
Self::from(protos.as_slice())
}
}

View File

@ -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<unsafe extern "C" fn(ssl: *const bffi::SSL, type_: c_int, value: c_int)>,
);
fn set_keylog_callback(
&mut self,
cb: Option<unsafe extern "C" fn(ssl: *const bffi::SSL, line: *const c_char)>,
);
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<T: HasPrivate>(&mut self, key: PKey<T>) -> 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 extern "C" fn(ssl: *const bffi::SSL, type_: c_int, value: c_int)>,
) {
unsafe { bffi::SSL_CTX_set_info_callback(self.as_ptr(), cb) }
}
fn set_keylog_callback(
&mut self,
cb: Option<unsafe extern "C" fn(ssl: *const bffi::SSL, line: *const c_char)>,
) {
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<T: HasPrivate>(&mut self, key: PKey<T>) -> 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<W: BufMut>(&self, out: &mut W) -> BoringResult;
fn decode<R: Buf>(ctx: &SslContextRef, r: &mut R) -> StdResult<SslSession, ErrorStack>;
}
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<W: BufMut>(&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<R: Buf>(ctx: &SslContextRef, r: &mut R) -> StdResult<SslSession, ErrorStack> {
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<bffi::ssl_encryption_level_t> 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<Level> 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())
}
}

View File

@ -0,0 +1,395 @@
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, 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 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 std::sync::LazyLock;
use tracing::{trace, warn};
/// Configuration for a client-side QUIC. Wraps around a BoringSSL [SslContext].
pub struct Config {
ctx: SslContext,
session_cache: Arc<dyn SessionCache>,
}
impl Config {
pub fn new() -> Result<Self> {
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 <https://www.rfc-editor.org/rfc/rfc9001.html#section-8.1>).
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)),
})
}
/// 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<dyn SessionCache> {
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<dyn SessionCache>) {
self.session_cache = session_cache;
}
/// Sets the ALPN protocols supported by the client. QUIC requires that
/// ALPN be used (see <https://www.rfc-editor.org/rfc/rfc9001.html#section-8.1>).
/// By default, the client will offer "h3".
pub fn set_alpn(&mut self, alpn_protocols: &[Vec<u8>]) -> Result<()> {
self.ctx
.set_alpn_protos(&AlpnProtocols::from(alpn_protocols).encode())?;
Ok(())
}
}
impl crypto::ClientConfig for Config {
fn start_session(
self: Arc<Self>,
version: u32,
server_name: &str,
params: &TransportParameters,
) -> StdResult<Box<dyn crypto::Session>, ConnectError> {
let version = QuicVersion::parse(version).unwrap();
Ok(Session::new(self, version, server_name, params)
.map_err(|_| ConnectError::EndpointStopping)?)
}
}
static SESSION_INDEX: LazyLock<c_int> = LazyLock::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<SessionState>,
server_name: Bytes,
session_cache: Arc<dyn SessionCache>,
zero_rtt_peer_params: Option<TransportParameters>,
handshake_data_available: bool,
handshake_data_sent: bool,
}
impl Session {
fn new(
cfg: Arc<Config>,
version: QuicVersion,
server_name: &str,
params: &TransportParameters,
) -> Result<Box<Self>> {
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::InvalidServerName(server_name.into()))?;
// Set the SNI hostname.
// TODO: should we validate the hostname?
ssl.set_hostname(server_name)
.map_err(|_| ConnectError::InvalidServerName(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)?,
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(&params)) {
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<Box<dyn Any>> {
self.state.handshake_data()
}
fn peer_identity(&self) -> Option<Box<dyn Any>> {
self.state.peer_identity()
}
fn early_crypto(&self) -> Option<(Box<dyn crypto::HeaderKey>, Box<dyn crypto::PacketKey>)> {
self.state.early_crypto()
}
fn early_data_accepted(&self) -> Option<bool> {
Some(self.state.ssl.early_data_accepted())
}
fn is_handshaking(&self) -> bool {
self.state.is_handshaking()
}
fn read_handshake(&mut self, plaintext: &[u8]) -> StdResult<bool, TransportError> {
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<Option<TransportParameters>, 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<u8>) -> Option<crypto::Keys> {
self.state.write_handshake(buf)
}
fn next_1rtt_keys(&mut self) -> Option<crypto::KeyPair<Box<dyn crypto::PacketKey>>> {
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()
}

150
quinn-boring/src/error.rs Normal file
View File

@ -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::other(msg))
}
}
/// Support conversion to CryptoError.
impl From<Error> for crypto::CryptoError {
fn from(_: Error) -> Self {
crypto::CryptoError
}
}
/// Support conversion to ConnectError.
impl From<Error> 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<ErrorStack> for Error {
fn from(e: ErrorStack) -> Self {
Error::SslError(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::IoError(e)
}
}
impl From<ConnectError> for Error {
fn from(e: ConnectError) -> Self {
Error::ConnectError(e)
}
}
impl From<TransportError> for Error {
fn from(e: TransportError) -> Self {
Error::TransportError(e)
}
}
/// The main result type for this (crypto boring) module.
pub type Result<T> = StdResult<T, Error>;
/// 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<T>(result: Result<T>) -> 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<T>(r: *mut T) -> Result<*mut T> {
if r.is_null() {
Err(Error::ssl())
} else {
Ok(r)
}
}

View File

@ -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> {
Self::new_for(Secret::random())
}
fn new_for(secret: Secret) -> Result<Self> {
// 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<dyn crypto::AeadKey> {
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);
}
}

129
quinn-boring/src/hkdf.rs Normal file
View File

@ -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 std::sync::LazyLock;
/// 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: LazyLock<Hkdf> = LazyLock::new(|| Hkdf(MessageDigest::sha256()));
static SHA384: LazyLock<Hkdf> = LazyLock::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 (<https://tools.ietf.org/html/rfc5869#section-2.2>),
/// 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<usize> {
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(())
}
}

74
quinn-boring/src/hmac.rs Normal file
View File

@ -0,0 +1,74 @@
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<u8>,
}
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::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 {})
}
}

529
quinn-boring/src/key.rs Normal file
View File

@ -0,0 +1,529 @@
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<T> {
/// 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<HeaderKey> {
#[inline]
pub(crate) fn as_crypto(&self) -> Result<crypto::KeyPair<Box<dyn crypto::HeaderKey>>> {
Ok(crypto::KeyPair {
local: self.local.as_crypto()?,
remote: self.remote.as_crypto()?,
})
}
}
impl KeyPair<PacketKey> {
#[inline]
pub(crate) fn as_crypto(&self) -> Result<crypto::KeyPair<Box<dyn crypto::PacketKey>>> {
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<HeaderKey>,
/// Packet protection keys
pub(crate) packet: KeyPair<PacketKey>,
}
impl Keys {
pub(crate) fn as_crypto(&self) -> Result<crypto::Keys> {
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<Self> {
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<Box<dyn crypto::HeaderKey>> {
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<AesHeaderKey> {
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<ChaChaHeaderKey> {
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::<u32>());
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<Self> {
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 {}
// EVP_AEAD_CTX_seal & EVP_AEAD_CTX_open allowed to be called concurrently on the same instance of EVP_AEAD_CTX
// https://github.com/google/boringssl/blob/master/include/openssl/aead.h#L278
unsafe impl Sync for AeadKey {}
impl AeadKey {
#[inline]
pub(crate) fn new(suite: &'static CipherSuite, key: Key) -> Result<Self> {
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<usize, crypto::CryptoError> {
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<u8>,
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])
}
}

300
quinn-boring/src/lib.rs Normal file
View File

@ -0,0 +1,300 @@
mod aead;
mod alert;
mod alpn;
mod bffi_ext;
mod client;
mod error;
mod handshake_token;
mod hkdf;
mod hmac;
mod key;
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 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<Vec<u8>>,
/// The server name specified by the client, if any
///
/// Always `None` for outgoing connections
pub server_name: Option<String>,
}
pub mod helpers {
use super::*;
use quinn_proto::crypto;
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<dyn crypto::ServerConfig>) -> Result<quinn::ServerConfig> {
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.
#[cfg(feature = "runtime-tokio")]
pub fn client_endpoint(addr: std::net::SocketAddr) -> std::io::Result<quinn::Endpoint> {
let socket = std::net::UdpSocket::bind(addr)?;
quinn::Endpoint::new(
default_endpoint_config(),
None,
socket,
Arc::new(quinn::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.
#[cfg(feature = "runtime-tokio")]
pub fn server_endpoint(
config: quinn::ServerConfig,
addr: std::net::SocketAddr,
) -> std::io::Result<quinn::Endpoint> {
let socket = std::net::UdpSocket::bind(addr)?;
quinn::Endpoint::new(
default_endpoint_config(),
Some(config),
socket,
Arc::new(quinn::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]);
}
}

120
quinn-boring/src/macros.rs Normal file
View File

@ -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::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<Self> {
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;

98
quinn-boring/src/retry.rs Normal file
View File

@ -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)
}
}

207
quinn-boring/src/secret.rs Normal file
View File

@ -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> {
HeaderKey::new(version, suite, self)
}
#[inline]
pub(crate) fn packet_key(
&self,
version: QuicVersion,
suite: &'static CipherSuite,
) -> Result<PacketKey> {
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 <https://datatracker.ietf.org/doc/html/rfc9001#name-initial-secrets>.
#[inline]
pub(crate) fn initial(
version: QuicVersion,
dst_cid: &ConnectionId,
side: Side,
) -> Result<Secrets> {
// 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<Keys> {
Ok(Keys {
header: self.header_keys()?,
packet: self.packet_keys()?,
})
}
#[inline]
pub(crate) fn header_keys(&self) -> Result<KeyPair<HeaderKey>> {
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<KeyPair<PacketKey>> {
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<KeyPair<PacketKey>> {
// 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<Secret>,
pub(crate) remote_secret: Option<Secret>,
}
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<Secrets> {
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
}
}

View File

@ -0,0 +1,313 @@
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, QuicSslContext};
use boring::ssl::{Ssl, SslContext, SslContextBuilder, SslMethod, SslVersion};
use boring_sys as bffi;
use bytes::{Bytes, BytesMut};
use foreign_types_shared::ForeignType;
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;
use std::sync::LazyLock;
/// Configuration for a server-side QUIC. Wraps around a BoringSSL [SslContext].
pub struct Config {
ctx: SslContext,
alpn_protocols: AlpnProtocols,
}
impl Config {
pub fn new() -> Result<Self> {
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_options(bffi::SSL_OP_CIPHER_SERVER_PREFERENCE as u32);
Ok(Self {
ctx,
alpn_protocols: AlpnProtocols::default(),
})
}
/// 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 <https://www.rfc-editor.org/rfc/rfc9001.html#section-8.1>).
///
/// If this method is not called, the server will default to accepting "h3".
pub fn set_alpn(&mut self, alpn_protocols: &[Vec<u8>]) -> Result<()> {
self.alpn_protocols = alpn_protocols.into();
Ok(())
}
}
impl crypto::ServerConfig for Config {
fn initial_keys(
&self,
version: u32,
dst_cid: &ConnectionId,
) -> StdResult<crypto::Keys, crypto::UnsupportedVersion> {
let version = QuicVersion::parse(version)?;
let secrets = Secrets::initial(version, dst_cid, Side::Server).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<Self>,
version: u32,
params: &TransportParameters,
) -> Box<dyn crypto::Session> {
let version = QuicVersion::parse(version).unwrap();
Session::new(self, version, params).unwrap()
}
}
static SESSION_INDEX: LazyLock<c_int> = LazyLock::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<SessionState>,
alpn: AlpnProtocols,
handshake_data_available: bool,
handshake_data_sent: bool,
}
impl Session {
fn new(
cfg: Arc<Config>,
version: QuicVersion,
params: &TransportParameters,
) -> Result<Box<Self>> {
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)?,
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<Box<dyn Any>> {
self.state.handshake_data()
}
fn peer_identity(&self) -> Option<Box<dyn Any>> {
self.state.peer_identity()
}
fn early_crypto(&self) -> Option<(Box<dyn crypto::HeaderKey>, Box<dyn crypto::PacketKey>)> {
self.state.early_crypto()
}
fn early_data_accepted(&self) -> Option<bool> {
None
}
fn is_handshaking(&self) -> bool {
self.state.is_handshaking()
}
fn read_handshake(&mut self, plaintext: &[u8]) -> StdResult<bool, TransportError> {
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<Option<TransportParameters>, TransportError> {
self.state.transport_parameters()
}
fn write_handshake(&mut self, buf: &mut Vec<u8>) -> Option<crypto::Keys> {
self.state.write_handshake(buf)
}
fn next_1rtt_keys(&mut self) -> Option<crypto::KeyPair<Box<dyn crypto::PacketKey>>> {
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()
}

View File

@ -0,0 +1,122 @@
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<Bytes>;
/// 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<Bytes> {
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<Self> {
// 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<Bytes> {
None
}
fn remove(&self, _: Bytes) {}
fn clear(&self) {}
}
pub struct SimpleCache {
cache: Mutex<LruCache<Bytes, Bytes>>,
}
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<Bytes> {
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()
}
}

View File

@ -0,0 +1,552 @@
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, 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 quinn_proto::{
crypto, transport_parameters::TransportParameters, ConnectionId, Side, TransportError,
};
use std::any::Any;
use std::ffi::c_int;
use std::io::Cursor;
use std::result::Result as StdResult;
use std::slice;
use std::sync::LazyLock;
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: LazyLock<c_int> = LazyLock::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,
alert: Option<TransportError>,
next_secrets: Option<Secrets>,
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) -> Result<Box<Self>> {
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,
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<Box<dyn Any>> {
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<crypto::KeyPair<Box<dyn crypto::PacketKey>>> {
self.next_secrets
.as_mut()
.map(|secrets| secrets.next_packet_keys().unwrap().as_crypto().unwrap())
}
#[inline]
pub(crate) fn transport_parameters(
&self,
) -> StdResult<Option<TransportParameters>, 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<u8>) -> Option<crypto::Keys> {
// 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<Box<dyn Any>> {
todo!()
}
#[inline]
pub(crate) fn early_crypto(
&self,
) -> Option<(Box<dyn crypto::HeaderKey>, Box<dyn crypto::PacketKey>)> {
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()
);
}
}
}
// Raw callbacks from BoringSSL
impl SessionState {
/// Called by the static callbacks to retrieve the instance pointer.
#[inline]
fn get_instance(ssl: *const bffi::SSL) -> &'static mut SessionState {
unsafe {
let data = bffi::SSL_get_ex_data(ssl, *SESSION_INDEX);
if data.is_null() {
panic!("BUG: SessionState instance missing")
}
&mut *(data as *mut SessionState)
}
}
extern "C" fn set_read_secret_callback(
ssl: *mut bffi::SSL,
level: bffi::ssl_encryption_level_t,
cipher: *const bffi::SSL_CIPHER,
secret: *const u8,
secret_len: usize,
) -> c_int {
let inst = Self::get_instance(ssl);
let level: Level = level.into();
let secret = unsafe { slice::from_raw_parts(secret, secret_len) };
let suite = CipherSuite::from_cipher(cipher).unwrap();
let secret = Secret::from(secret);
map_cb_result(inst.on_set_read_secret(level, suite, secret))
}
extern "C" fn set_write_secret_callback(
ssl: *mut bffi::SSL,
level: bffi::ssl_encryption_level_t,
cipher: *const bffi::SSL_CIPHER,
secret: *const u8,
secret_len: usize,
) -> c_int {
let inst = Self::get_instance(ssl);
let level: Level = level.into();
let secret = unsafe { slice::from_raw_parts(secret, secret_len) };
let suite = CipherSuite::from_cipher(cipher).unwrap();
let secret = Secret::from(secret);
map_cb_result(inst.on_set_write_secret(level, suite, secret))
}
extern "C" fn add_handshake_data_callback(
ssl: *mut bffi::SSL,
level: bffi::ssl_encryption_level_t,
data: *const u8,
len: usize,
) -> c_int {
let inst = Self::get_instance(ssl);
let level: Level = level.into();
let data = unsafe { slice::from_raw_parts(data, len) };
map_cb_result(inst.on_add_handshake_data(level, data))
}
extern "C" fn flush_flight_callback(ssl: *mut bffi::SSL) -> c_int {
let inst = Self::get_instance(ssl);
map_cb_result(inst.on_flush_flight())
}
extern "C" fn send_alert_callback(
ssl: *mut bffi::SSL,
level: bffi::ssl_encryption_level_t,
alert: u8,
) -> c_int {
let inst = Self::get_instance(ssl);
let level: Level = level.into();
map_cb_result(inst.on_send_alert(level, Alert::from(alert)))
}
pub(crate) extern "C" fn info_callback(ssl: *const bffi::SSL, type_: c_int, value: c_int) {
let inst = Self::get_instance(ssl);
inst.on_info(type_, value);
}
}
pub(crate) struct LevelState {
pub(crate) builder: SecretsBuilder,
pub(crate) write_buffer: BytesMut,
}
impl LevelState {
#[inline]
fn new(version: QuicVersion, level: Level, ssl: &Ssl) -> Self {
let capacity = ssl.quic_max_handshake_flight_len(level);
Self {
builder: SecretsBuilder::new(version),
write_buffer: BytesMut::with_capacity(capacity),
}
}
}

105
quinn-boring/src/suite.rs Normal file
View File

@ -0,0 +1,105 @@
use crate::aead::Aead;
use crate::error::{Error, Result};
use crate::hkdf::Hkdf;
use boring_sys as bffi;
use std::fmt::{Debug, Formatter};
use std::sync::LazyLock;
// For AEAD_AES_128_GCM and AEAD_AES_256_GCM ... endpoints that do not send
// packets larger than 2^11 bytes cannot protect more than 2^28 packets.
// https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-confidentiality-limit
const AES_CONFIDENTIALITY_LIMIT: u64 = 2u64.pow(28);
// For AEAD_CHACHA20_POLY1305, the confidentiality limit is greater than the
// number of possible packets (2^62) and so can be disregarded.
// https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-limits-on-aead-usage
const CHACHA20_POLY1305_CONFIDENTIALITY_LIMIT: u64 = u64::MAX;
// For AEAD_AES_128_GCM ... endpoints that do not attempt to remove
// protection from packets larger than 2^11 bytes can attempt to remove
// protection from at most 2^57 packets.
// For AEAD_AES_256_GCM [the limit] is substantially larger than the limit for
// AEAD_AES_128_GCM. However, this document recommends that the same limit be
// applied to both functions as either limit is acceptably large.
// https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-integrity-limit
const AES_INTEGRITY_LIMIT: u64 = 2u64.pow(57);
// For AEAD_CHACHA20_POLY1305, the integrity limit is 2^36 invalid packets.
// https://quicwg.org/base-drafts/draft-ietf-quic-tls.html#name-limits-on-aead-usage
const CHACHA20_POLY1305_INTEGRITY_LIMIT: u64 = 2u64.pow(36);
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum ID {
Aes128GcmSha256,
Aes256GcmSha384,
Chacha20Poly1305Sha256,
}
#[derive(Eq, PartialEq)]
pub(crate) struct CipherSuite {
pub(crate) id: ID,
pub(crate) hkdf: Hkdf,
pub(crate) aead: &'static Aead,
pub(crate) confidentiality_limit: u64,
pub(crate) integrity_limit: u64,
}
impl Debug for CipherSuite {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.id, f)
}
}
static AES128_GCM_SHA256: LazyLock<CipherSuite> = LazyLock::new(|| CipherSuite {
id: ID::Aes128GcmSha256,
hkdf: Hkdf::sha256(),
aead: Aead::aes128_gcm(),
confidentiality_limit: AES_CONFIDENTIALITY_LIMIT,
integrity_limit: AES_INTEGRITY_LIMIT,
});
static AES256_GCM_SHA384: LazyLock<CipherSuite> = LazyLock::new(|| CipherSuite {
id: ID::Aes256GcmSha384,
hkdf: Hkdf::sha384(),
aead: Aead::aes256_gcm(),
confidentiality_limit: AES_CONFIDENTIALITY_LIMIT,
integrity_limit: AES_INTEGRITY_LIMIT,
});
static CHACHA20_POLY1305_SHA256: LazyLock<CipherSuite> = LazyLock::new(|| CipherSuite {
id: ID::Chacha20Poly1305Sha256,
hkdf: Hkdf::sha256(),
aead: Aead::chacha20_poly1305(),
confidentiality_limit: CHACHA20_POLY1305_CONFIDENTIALITY_LIMIT,
integrity_limit: CHACHA20_POLY1305_INTEGRITY_LIMIT,
});
unsafe impl Send for CipherSuite {}
unsafe impl Sync for CipherSuite {}
impl CipherSuite {
#[inline]
pub(crate) fn aes128_gcm_sha256() -> &'static Self {
&AES128_GCM_SHA256
}
#[inline]
pub(crate) fn aes256_gcm_sha384() -> &'static Self {
&AES256_GCM_SHA384
}
#[inline]
pub(crate) fn chacha20_poly1305_sha256() -> &'static Self {
&CHACHA20_POLY1305_SHA256
}
#[inline]
pub(crate) fn from_cipher(cipher: *const bffi::SSL_CIPHER) -> Result<&'static Self> {
match unsafe { bffi::SSL_CIPHER_get_id(cipher) } as i32 {
bffi::TLS1_CK_AES_128_GCM_SHA256 => Ok(Self::aes128_gcm_sha256()),
bffi::TLS1_CK_AES_256_GCM_SHA384 => Ok(Self::aes256_gcm_sha384()),
bffi::TLS1_CK_CHACHA20_POLY1305_SHA256 => Ok(Self::chacha20_poly1305_sha256()),
id => Err(Error::invalid_input(format!("invalid cipher id: {id}"))),
}
}
}

151
quinn-boring/src/version.rs Normal file
View File

@ -0,0 +1,151 @@
use quinn_proto::crypto;
use std::result::Result as StdResult;
/// QUIC protocol version
///
/// Governs version-specific behavior in the TLS layer
// TODO: add support for draft version 2.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QuicVersion {
V1Draft29,
V1Draft30,
V1Draft31,
V1Draft32,
V1Draft33,
V1Draft34,
/// First stable RFC version.
V1,
}
impl Default for QuicVersion {
fn default() -> Self {
Self::V1
}
}
impl QuicVersion {
const DRAFT_INDICATOR: u32 = 0xff00_0000;
const VERSION_1_DRAFT_29: u32 = Self::DRAFT_INDICATOR | 29;
const VERSION_1_DRAFT_30: u32 = Self::DRAFT_INDICATOR | 30;
const VERSION_1_DRAFT_31: u32 = Self::DRAFT_INDICATOR | 31;
const VERSION_1_DRAFT_32: u32 = Self::DRAFT_INDICATOR | 32;
const VERSION_1_DRAFT_33: u32 = Self::DRAFT_INDICATOR | 33;
const VERSION_1_DRAFT_34: u32 = Self::DRAFT_INDICATOR | 34;
const VERSION_1: u32 = 1;
/// Returns the default list of supported quic versions.
pub fn default_supported_versions() -> Vec<u32> {
let mut out = Vec::new();
for v in [
Self::V1,
Self::V1Draft34,
Self::V1Draft33,
Self::V1Draft32,
Self::V1Draft31,
Self::V1Draft30,
Self::V1Draft29,
] {
out.push(v.label());
}
out
}
pub(crate) fn parse(version: u32) -> StdResult<Self, crypto::UnsupportedVersion> {
match version {
Self::VERSION_1_DRAFT_29 => Ok(Self::V1Draft29),
Self::VERSION_1_DRAFT_30 => Ok(Self::V1Draft30),
Self::VERSION_1_DRAFT_31 => Ok(Self::V1Draft31),
Self::VERSION_1_DRAFT_32 => Ok(Self::V1Draft32),
Self::VERSION_1_DRAFT_33 => Ok(Self::V1Draft33),
Self::VERSION_1_DRAFT_34 => Ok(Self::V1Draft34),
Self::VERSION_1 => Ok(Self::V1),
_ => Err(crypto::UnsupportedVersion),
}
}
pub(crate) fn label(&self) -> u32 {
match self {
Self::V1Draft29 => Self::VERSION_1_DRAFT_29,
Self::V1Draft30 => Self::VERSION_1_DRAFT_30,
Self::V1Draft31 => Self::VERSION_1_DRAFT_31,
Self::V1Draft32 => Self::VERSION_1_DRAFT_32,
Self::V1Draft33 => Self::VERSION_1_DRAFT_33,
Self::V1Draft34 => Self::VERSION_1_DRAFT_34,
Self::V1 => Self::VERSION_1,
}
}
pub(crate) fn initial_salt(&self) -> &'static [u8] {
match self {
Self::V1Draft29 | Self::V1Draft30 | Self::V1Draft31 | Self::V1Draft32 => &[
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#section-5.2
0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61,
0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99,
],
Self::V1Draft33 | Self::V1Draft34 | Self::V1 => &[
// https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets
0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8,
0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a,
],
}
}
pub(crate) fn retry_integrity_key(&self) -> &'static [u8] {
match self {
Self::V1Draft29 | Self::V1Draft30 | Self::V1Draft31 | Self::V1Draft32 => &[
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#section-5.8
0xcc, 0xce, 0x18, 0x7e, 0xd0, 0x9a, 0x09, 0xd0, 0x57, 0x28, 0x15, 0x5a, 0x6c, 0xb9,
0x6b, 0xe1,
],
Self::V1Draft33 | Self::V1Draft34 | Self::V1 => &[
// https://datatracker.ietf.org/doc/html/rfc9001#name-retry-packet-integrity
0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68,
0xc8, 0x4e,
],
}
}
pub(crate) fn retry_integrity_nonce(&self) -> &'static [u8] {
match self {
Self::V1Draft29 | Self::V1Draft30 | Self::V1Draft31 | Self::V1Draft32 => &[
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#section-5.8
0xe5, 0x49, 0x30, 0xf9, 0x7f, 0x21, 0x36, 0xf0, 0x53, 0x0a, 0x8c, 0x1c,
],
Self::V1Draft33 | Self::V1Draft34 | Self::V1 => &[
// https://datatracker.ietf.org/doc/html/rfc9001#name-retry-packet-integrity
0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb,
],
}
}
/// Indicates whether this version uses the legacy TLS extension codepoint.
pub(crate) fn uses_legacy_extension(&self) -> bool {
match self {
Self::V1Draft29
| Self::V1Draft30
| Self::V1Draft31
| Self::V1Draft32
| Self::V1Draft33
| Self::V1Draft34 => true,
Self::V1 => false,
}
}
pub(crate) fn key_label(&self) -> &'static [u8] {
b"quic key"
}
pub(crate) fn iv_label(&self) -> &'static [u8] {
b"quic iv"
}
pub(crate) fn header_key_label(&self) -> &'static [u8] {
b"quic hp"
}
pub(crate) fn key_update_label(&self) -> &'static [u8] {
b"quic ku"
}
}