use std::net::{Ipv6Addr, SocketAddr};
use std::sync::Arc;
use quinn::crypto::rustls::{QuicClientConfig, QuicServerConfig};
use quinn::rustls::client::danger::{
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
};
use quinn::rustls::pki_types::pem::PemObject;
use quinn::rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
use quinn::rustls::{
ClientConfig as RustlsClientConfig, DigitallySignedStruct, ServerConfig as RustlsServerConfig,
SignatureScheme,
};
use quinn::{ClientConfig, ServerConfig};
use subtle::ConstantTimeEq;
use crate::KeyState;
pub const QUIC_EXPORTER_LABEL: &[u8] = b"EXPORTER-Channel-Binding";
pub const QUIC_EXPORTER_CONTEXT: &[u8] = b"auths-quic-channel-binding-v1";
pub const QUIC_EXPORTER_LEN: usize = 32;
#[derive(Debug, thiserror::Error)]
pub enum QuicTransportError {
#[error("parse PEM material: {0}")]
Pem(String),
#[error("build QUIC TLS config: {0}")]
Config(String),
#[error("QUIC channel binding unavailable: {0}")]
ExporterUnavailable(String),
}
fn parse_cert_chain(cert_pem: &str) -> Result<Vec<CertificateDer<'static>>, QuicTransportError> {
CertificateDer::pem_slice_iter(cert_pem.as_bytes())
.collect::<Result<Vec<_>, _>>()
.map_err(|e| QuicTransportError::Pem(format!("certificate chain: {e}")))
}
fn parse_private_key(key_pem: &str) -> Result<PrivateKeyDer<'static>, QuicTransportError> {
PrivateKeyDer::from_pem_slice(key_pem.as_bytes())
.map_err(|e| QuicTransportError::Pem(format!("private key: {e}")))
}
pub fn quic_server_config(
cert_pem: &str,
key_pem: &str,
) -> Result<ServerConfig, QuicTransportError> {
let certs = parse_cert_chain(cert_pem)?;
let key = parse_private_key(key_pem)?;
let mut tls = RustlsServerConfig::builder_with_provider(crypto_provider())
.with_protocol_versions(&[&quinn::rustls::version::TLS13])
.map_err(|e| QuicTransportError::Config(format!("TLS1.3 server: {e}")))?
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| QuicTransportError::Config(format!("server cert: {e}")))?;
tls.alpn_protocols = vec![b"h3".to_vec()];
let quic_tls = QuicServerConfig::try_from(tls)
.map_err(|e| QuicTransportError::Config(format!("quic server tls: {e}")))?;
Ok(ServerConfig::with_crypto(Arc::new(quic_tls)))
}
pub fn quic_client_config() -> Result<ClientConfig, QuicTransportError> {
let mut tls = RustlsClientConfig::builder_with_provider(crypto_provider())
.with_protocol_versions(&[&quinn::rustls::version::TLS13])
.map_err(|e| QuicTransportError::Config(format!("TLS1.3 client: {e}")))?
.dangerous()
.with_custom_certificate_verifier(Arc::new(KelRootedVerifier))
.with_no_client_auth();
tls.alpn_protocols = vec![b"h3".to_vec()];
let quic_tls = QuicClientConfig::try_from(tls)
.map_err(|e| QuicTransportError::Config(format!("quic client tls: {e}")))?;
Ok(ClientConfig::new(Arc::new(quic_tls)))
}
pub fn quic_channel_binding(
conn: &quinn::Connection,
) -> Result<[u8; QUIC_EXPORTER_LEN], QuicTransportError> {
let mut material = [0u8; QUIC_EXPORTER_LEN];
conn.export_keying_material(&mut material, QUIC_EXPORTER_LABEL, QUIC_EXPORTER_CONTEXT)
.map_err(|e| QuicTransportError::ExporterUnavailable(format!("{e:?}")))?;
Ok(material)
}
fn crypto_provider() -> Arc<quinn::rustls::crypto::CryptoProvider> {
Arc::new(quinn::rustls::crypto::aws_lc_rs::default_provider())
}
#[derive(Debug)]
struct KelRootedVerifier;
impl ServerCertVerifier for KelRootedVerifier {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, quinn::rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, quinn::rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, quinn::rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
vec![
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ED25519,
SignatureScheme::RSA_PSS_SHA256,
]
}
}
#[derive(Debug, Clone)]
pub struct QuicLoopbackOutcome {
pub did_keri: String,
pub binding_agrees: bool,
pub channel_binding_hex: String,
pub channel_binding_len: usize,
}
pub async fn quic_loopback_compose(
cert_pem: &str,
key_pem: &str,
state: &KeyState,
) -> Result<QuicLoopbackOutcome, QuicTransportError> {
let server_cfg = quic_server_config(cert_pem, key_pem)?;
let loopback = SocketAddr::from((Ipv6Addr::LOCALHOST, 0));
let server = quinn::Endpoint::server(server_cfg, loopback)
.map_err(|e| QuicTransportError::Config(format!("bind QUIC server: {e}")))?;
let server_addr = server
.local_addr()
.map_err(|e| QuicTransportError::Config(format!("server local addr: {e}")))?;
let mut client = quinn::Endpoint::client(loopback)
.map_err(|e| QuicTransportError::Config(format!("bind QUIC client: {e}")))?;
client.set_default_client_config(quic_client_config()?);
let accept = async {
let incoming = server
.accept()
.await
.ok_or_else(|| QuicTransportError::Config("no inbound QUIC connection".to_string()))?;
incoming
.await
.map_err(|e| QuicTransportError::Config(format!("server handshake: {e}")))
};
let connect = async {
client
.connect(server_addr, "localhost")
.map_err(|e| QuicTransportError::Config(format!("client connect: {e}")))?
.await
.map_err(|e| QuicTransportError::Config(format!("client handshake: {e}")))
};
let (server_conn, client_conn) = tokio::try_join!(accept, connect)?;
let client_cb = quic_channel_binding(&client_conn)?;
let server_cb = quic_channel_binding(&server_conn)?;
let binding_agrees = bool::from(client_cb.ct_eq(&server_cb));
let chain = client_conn
.peer_identity()
.and_then(|id| id.downcast::<Vec<CertificateDer<'static>>>().ok())
.ok_or_else(|| {
QuicTransportError::Config("server presented no certificate over QUIC".to_string())
})?;
let leaf_der = chain
.first()
.ok_or_else(|| QuicTransportError::Config("empty certificate chain".to_string()))?;
let observed_pem = pem_from_der(leaf_der);
let binding = crate::verify_binds_to_key_state(&observed_pem, state).map_err(|e| {
QuicTransportError::Config(format!("served leaf does not bind to KEL: {e}"))
})?;
client.close(0u32.into(), b"done");
server.close(0u32.into(), b"done");
Ok(QuicLoopbackOutcome {
did_keri: binding.did_keri(),
binding_agrees,
channel_binding_hex: hex::encode(client_cb),
channel_binding_len: QUIC_EXPORTER_LEN,
})
}
fn pem_from_der(der: &CertificateDer<'_>) -> String {
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD.encode(der.as_ref());
let mut pem = String::from("-----BEGIN CERTIFICATE-----\n");
for chunk in b64.as_bytes().chunks(64) {
pem.push_str(&String::from_utf8_lossy(chunk));
pem.push('\n');
}
pem.push_str("-----END CERTIFICATE-----\n");
pem
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::net::{Ipv6Addr, SocketAddr};
use quinn::{Endpoint, Incoming};
use crate::tls_cert::{issue_kel_rooted_cert, verify_binds_to_key_state};
use crate::types::{CesrKey, Prefix, Said, Threshold};
use crate::{IssuedCert, KeriPublicKey, KeyState};
fn sample_state() -> KeyState {
let key = KeriPublicKey::ed25519(&[9u8; 32])
.unwrap()
.to_qb64()
.unwrap();
KeyState::from_inception(
Prefix::new_unchecked("EQuicAidAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string()),
vec![CesrKey::new_unchecked(key)],
vec![Said::new_unchecked("ENext0".to_string())],
Threshold::Simple(1),
Threshold::Simple(1),
Said::new_unchecked("ETipQuic000000000000000000000000000000000000".to_string()),
vec![],
Threshold::Simple(0),
vec![],
)
}
fn kel_rooted_leaf() -> IssuedCert {
issue_kel_rooted_cert(
&sample_state(),
&["localhost".to_string(), "::1".to_string()],
)
.expect("mint KEL-rooted leaf")
}
fn server_endpoint(leaf: &IssuedCert) -> (Endpoint, SocketAddr) {
let cfg = quic_server_config(&leaf.cert_pem, &leaf.key_pem).expect("server cfg");
let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, 0));
let ep = Endpoint::server(cfg, addr).expect("server endpoint");
let bound = ep.local_addr().expect("local addr");
(ep, bound)
}
async fn handshake(
client: &Endpoint,
server: &Endpoint,
server_addr: SocketAddr,
) -> (
[u8; QUIC_EXPORTER_LEN],
[u8; QUIC_EXPORTER_LEN],
Vec<CertificateDer<'static>>,
) {
let accept = async {
let incoming: Incoming = server.accept().await.expect("incoming");
incoming.await.expect("server connection")
};
let connect = async {
client
.connect(server_addr, "localhost")
.expect("connect")
.await
.expect("client connection")
};
let (server_conn, client_conn) = tokio::join!(accept, connect);
let client_cb = quic_channel_binding(&client_conn).expect("client binding");
let server_cb = quic_channel_binding(&server_conn).expect("server binding");
let peer_chain = client_conn
.peer_identity()
.expect("peer identity")
.downcast::<Vec<CertificateDer<'static>>>()
.expect("cert chain");
(client_cb, server_cb, *peer_chain)
}
fn client_endpoint() -> Endpoint {
let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, 0));
let mut ep = Endpoint::client(addr).expect("client endpoint");
ep.set_default_client_config(quic_client_config().expect("client cfg"));
ep
}
#[tokio::test]
async fn server_serves_kel_rooted_leaf_over_quic() {
let leaf = kel_rooted_leaf();
let (server, addr) = server_endpoint(&leaf);
let client = client_endpoint();
let (_c, _s, chain) = handshake(&client, &server, addr).await;
assert!(
!chain.is_empty(),
"server presented no certificate over QUIC"
);
let observed_pem = pem_from_der(&chain[0]);
let binding = verify_binds_to_key_state(&observed_pem, &sample_state())
.expect("served leaf must bind to the KEL");
assert_eq!(
binding.did_keri(),
"did:keri:EQuicAidAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
);
}
#[tokio::test]
async fn both_endpoints_derive_the_same_binding() {
let leaf = kel_rooted_leaf();
let (server, addr) = server_endpoint(&leaf);
let client = client_endpoint();
let (client_cb, server_cb, _chain) = handshake(&client, &server, addr).await;
assert_eq!(
client_cb, server_cb,
"both ends of one QUIC connection must derive the same channel binding"
);
}
#[tokio::test]
async fn independent_connections_derive_distinct_bindings() {
let leaf = kel_rooted_leaf();
let (server, addr) = server_endpoint(&leaf);
let client = client_endpoint();
let (cb1, _s1, _c1) = handshake(&client, &server, addr).await;
let (cb2, _s2, _c2) = handshake(&client, &server, addr).await;
assert_ne!(
cb1, cb2,
"two independent QUIC connections must derive distinct bindings"
);
}
fn pem_from_der(der: &CertificateDer<'_>) -> String {
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD.encode(der.as_ref());
let mut pem = String::from("-----BEGIN CERTIFICATE-----\n");
for chunk in b64.as_bytes().chunks(64) {
pem.push_str(std::str::from_utf8(chunk).unwrap());
pem.push('\n');
}
pem.push_str("-----END CERTIFICATE-----\n");
pem
}
}