feat(boring): adapt boring2 for quinn (#87)
This commit is contained in:
parent
6bcc6af083
commit
d9a1d9442e
|
|
@ -101,21 +101,25 @@ jobs:
|
|||
rust: stable
|
||||
os: ubuntu-latest
|
||||
check_only: true
|
||||
extra_test_args: --workspace --exclude quinn-boring2
|
||||
- thing: arm64-android
|
||||
target: aarch64-linux-android
|
||||
rust: stable
|
||||
os: ubuntu-latest
|
||||
check_only: true
|
||||
extra_test_args: --workspace --exclude quinn-boring2
|
||||
- thing: i686-android
|
||||
target: i686-linux-android
|
||||
rust: stable
|
||||
os: ubuntu-latest
|
||||
check_only: true
|
||||
extra_test_args: --workspace --exclude quinn-boring2
|
||||
- thing: x86_64-android
|
||||
target: x86_64-linux-android
|
||||
rust: stable
|
||||
os: ubuntu-latest
|
||||
check_only: true
|
||||
extra_test_args: --workspace --exclude quinn-boring2
|
||||
- thing: aarch64-ios
|
||||
target: aarch64-apple-ios
|
||||
os: macos-latest
|
||||
|
|
@ -319,6 +323,8 @@ jobs:
|
|||
sleep 10
|
||||
echo "=== Publishing compio-boring2... ==="
|
||||
(cd compio-boring && cargo publish)
|
||||
echo "=== Publishing quinn-boring2... ==="
|
||||
(cd quinn-boring && cargo publish)
|
||||
|
||||
- name: Upload binaries to GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ members = [
|
|||
"boring-sys",
|
||||
"tokio-boring",
|
||||
"compio-boring",
|
||||
"quinn-boring",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
/target
|
||||
/Cargo.lock
|
||||
|
||||
.idea
|
||||
.DS_Store
|
||||
.vscode
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).
|
||||
|
|
@ -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())?)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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(¶ms)) {
|
||||
Ok(params) => params,
|
||||
Err(e) => {
|
||||
warn!("failed parsing server transport parameters: {:?}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("failed caching session: server transport parameters are not available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Encode the session cache entry, including both the session and the server params.
|
||||
let entry = Entry { session, params };
|
||||
match entry.encode() {
|
||||
Ok(value) => {
|
||||
// Cache the session.
|
||||
self.session_cache.put(self.server_name.clone(), value)
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed caching session: unable to encode entry: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by the static callbacks to retrieve the instance pointer.
|
||||
#[inline]
|
||||
fn get_instance(ssl: *const bffi::SSL) -> &'static mut Session {
|
||||
unsafe {
|
||||
let data = bffi::SSL_get_ex_data(ssl, *SESSION_INDEX);
|
||||
if data.is_null() {
|
||||
panic!("BUG: Session instance missing")
|
||||
}
|
||||
&mut *(data as *mut Session)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw callback from BoringSSL.
|
||||
extern "C" fn new_session_callback(
|
||||
ssl: *mut bffi::SSL,
|
||||
session: *mut bffi::SSL_SESSION,
|
||||
) -> c_int {
|
||||
let inst = Self::get_instance(ssl);
|
||||
let session = unsafe { SslSession::from_ptr(session) };
|
||||
inst.on_new_session(session);
|
||||
|
||||
// Return 1 to indicate we've taken ownership of the session.
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
impl crypto::Session for Session {
|
||||
fn initial_keys(&self, dcid: &ConnectionId, side: Side) -> crypto::Keys {
|
||||
self.state.initial_keys(dcid, side)
|
||||
}
|
||||
|
||||
fn handshake_data(&self) -> Option<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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {})
|
||||
}
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue