Replace address-only trust and pushed peer state with installation identities, SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned protocol-8 pulls. The runtime now owns each network generation and all admitted work through shutdown. Add exact bundled content identities, reproducible manifest publishing, capability-confined downloads, streaming BLAKE3 verification, quarantine and retry, and crash-recoverable download and install transactions. Ship generated fixture catalogs and fail closed when production manifests are absent. The Tauri backend exposes durable sharing policy, redacted identity state, and attempt-keyed transfer snapshots. Frontend consumption follows in the next commit. Repository-wide test certificates and protocol-7 paths are removed. BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts; protocol-7 frames and shared-certificate identities are no longer accepted. Test Plan: - `just test` -- passed on the completed stack (708 workspace tests) - `just clippy` -- passed on the completed stack - `just build` -- passed with fixture catalogs on the completed stack - `just catalog-check-production` -- failed closed because the external production manifest corpus is absent - `git diff --cached --check` -- passed
383 lines
12 KiB
Rust
383 lines
12 KiB
Rust
//! TLS 1.3 responder identity for QUIC.
|
|
|
|
use std::sync::Arc;
|
|
#[cfg(test)]
|
|
use std::{
|
|
collections::HashMap,
|
|
sync::{
|
|
LazyLock,
|
|
Mutex,
|
|
Weak,
|
|
atomic::{AtomicUsize, Ordering},
|
|
},
|
|
thread::{self, ThreadId},
|
|
};
|
|
|
|
use lanspread_proto::{ALPN_PROTOCOL, PeerId};
|
|
#[cfg(test)]
|
|
use rustls::sign::{CertifiedKey, SingleCertAndKey};
|
|
use rustls::{
|
|
CertificateError,
|
|
ClientConfig,
|
|
DigitallySignedStruct,
|
|
Error as RustlsError,
|
|
ServerConfig,
|
|
SignatureScheme,
|
|
client::{
|
|
Resumption,
|
|
danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
|
},
|
|
crypto::WebPkiSupportedAlgorithms,
|
|
pki_types::{CertificateDer, ServerName, UnixTime},
|
|
server::{NoServerSessionStorage, ParsedCertificate},
|
|
};
|
|
use s2n_quic::provider::tls::rustls::{
|
|
client::Client as S2nRustlsClient,
|
|
server::Server as S2nRustlsServer,
|
|
};
|
|
|
|
use crate::identity::{PeerIdentity, peer_id_from_spki, server_name_for_peer};
|
|
|
|
pub(crate) fn protocol_alpn() -> Vec<u8> {
|
|
ALPN_PROTOCOL.to_vec()
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct PeerIdServerCertVerifier {
|
|
supported_algorithms: WebPkiSupportedAlgorithms,
|
|
}
|
|
|
|
impl ServerCertVerifier for PeerIdServerCertVerifier {
|
|
fn verify_server_cert(
|
|
&self,
|
|
end_entity: &CertificateDer<'_>,
|
|
intermediates: &[CertificateDer<'_>],
|
|
server_name: &ServerName<'_>,
|
|
ocsp_response: &[u8],
|
|
_now: UnixTime,
|
|
) -> Result<ServerCertVerified, RustlsError> {
|
|
if !intermediates.is_empty() || !ocsp_response.is_empty() {
|
|
return Err(application_verification_failure());
|
|
}
|
|
|
|
let expected_peer = expected_peer_from_server_name(server_name)?;
|
|
let parsed = ParsedCertificate::try_from(end_entity)?;
|
|
let spki = parsed.subject_public_key_info();
|
|
let actual_peer =
|
|
peer_id_from_spki(spki.as_ref()).map_err(|_| application_verification_failure())?;
|
|
if actual_peer != expected_peer {
|
|
return Err(application_verification_failure());
|
|
}
|
|
|
|
rustls::client::verify_server_name(&parsed, server_name)?;
|
|
Ok(ServerCertVerified::assertion())
|
|
}
|
|
|
|
fn verify_tls12_signature(
|
|
&self,
|
|
_message: &[u8],
|
|
_cert: &CertificateDer<'_>,
|
|
_dss: &DigitallySignedStruct,
|
|
) -> Result<HandshakeSignatureValid, RustlsError> {
|
|
Err(RustlsError::General(
|
|
"TLS 1.2 CertificateVerify is disabled".to_owned(),
|
|
))
|
|
}
|
|
|
|
fn verify_tls13_signature(
|
|
&self,
|
|
message: &[u8],
|
|
cert: &CertificateDer<'_>,
|
|
dss: &DigitallySignedStruct,
|
|
) -> Result<HandshakeSignatureValid, RustlsError> {
|
|
if dss.scheme != SignatureScheme::ED25519 {
|
|
return Err(RustlsError::General(
|
|
"unexpected TLS 1.3 CertificateVerify scheme".to_owned(),
|
|
));
|
|
}
|
|
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported_algorithms)
|
|
}
|
|
|
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
|
vec![SignatureScheme::ED25519]
|
|
}
|
|
}
|
|
|
|
pub(crate) fn client_provider() -> Result<S2nRustlsClient, RustlsError> {
|
|
let provider = rustls::crypto::aws_lc_rs::default_provider();
|
|
let verifier: Arc<dyn ServerCertVerifier> = Arc::new(PeerIdServerCertVerifier {
|
|
supported_algorithms: provider.signature_verification_algorithms,
|
|
});
|
|
#[cfg(test)]
|
|
let verifier = decorate_verifier_for_current_test(verifier);
|
|
let mut config = ClientConfig::builder_with_provider(Arc::new(provider))
|
|
.with_protocol_versions(&[&rustls::version::TLS13])?
|
|
.dangerous()
|
|
.with_custom_certificate_verifier(verifier)
|
|
.with_no_client_auth();
|
|
config.alpn_protocols = vec![protocol_alpn()];
|
|
config.check_selected_alpn = true;
|
|
config.enable_sni = true;
|
|
config.resumption = Resumption::disabled();
|
|
config.enable_early_data = false;
|
|
Ok(S2nRustlsClient::from(config))
|
|
}
|
|
|
|
pub(crate) fn server_provider(identity: &PeerIdentity) -> Result<S2nRustlsServer, RustlsError> {
|
|
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
|
let mut config = ServerConfig::builder_with_provider(provider)
|
|
.with_protocol_versions(&[&rustls::version::TLS13])?
|
|
.with_no_client_auth()
|
|
.with_single_cert(vec![identity.certificate()], identity.private_key())?;
|
|
harden_server_config(&mut config);
|
|
Ok(S2nRustlsServer::from(config))
|
|
}
|
|
|
|
fn harden_server_config(config: &mut ServerConfig) {
|
|
config.alpn_protocols = vec![protocol_alpn()];
|
|
config.session_storage = Arc::new(NoServerSessionStorage {});
|
|
config.send_tls13_tickets = 0;
|
|
config.max_tls13_tickets = 0;
|
|
config.max_early_data_size = 0;
|
|
config.send_half_rtt_data = false;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn hostile_mismatched_server_provider(
|
|
certificate_identity: &PeerIdentity,
|
|
signing_identity: &PeerIdentity,
|
|
) -> Result<S2nRustlsServer, RustlsError> {
|
|
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
|
let signer =
|
|
rustls::crypto::aws_lc_rs::sign::any_supported_type(&signing_identity.private_key())?;
|
|
let certified_key = CertifiedKey::new(vec![certificate_identity.certificate()], signer);
|
|
let resolver = Arc::new(SingleCertAndKey::from(certified_key));
|
|
let mut config = ServerConfig::builder_with_provider(provider)
|
|
.with_protocol_versions(&[&rustls::version::TLS13])?
|
|
.with_no_client_auth()
|
|
.with_cert_resolver(resolver);
|
|
harden_server_config(&mut config);
|
|
Ok(S2nRustlsServer::from(config))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[derive(Debug, Default)]
|
|
struct VerificationCounts {
|
|
certificate_calls: AtomicUsize,
|
|
certificate_accepts: AtomicUsize,
|
|
tls12_calls: AtomicUsize,
|
|
tls13_calls: AtomicUsize,
|
|
tls13_accepts: AtomicUsize,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) struct VerificationSnapshot {
|
|
pub(crate) certificate_calls: usize,
|
|
pub(crate) certificate_accepts: usize,
|
|
pub(crate) tls12_calls: usize,
|
|
pub(crate) tls13_calls: usize,
|
|
pub(crate) tls13_accepts: usize,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl VerificationCounts {
|
|
fn snapshot(&self) -> VerificationSnapshot {
|
|
VerificationSnapshot {
|
|
certificate_calls: self.certificate_calls.load(Ordering::SeqCst),
|
|
certificate_accepts: self.certificate_accepts.load(Ordering::SeqCst),
|
|
tls12_calls: self.tls12_calls.load(Ordering::SeqCst),
|
|
tls13_calls: self.tls13_calls.load(Ordering::SeqCst),
|
|
tls13_accepts: self.tls13_accepts.load(Ordering::SeqCst),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[derive(Debug)]
|
|
struct CountingVerifier {
|
|
inner: Arc<dyn ServerCertVerifier>,
|
|
counts: Arc<VerificationCounts>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl ServerCertVerifier for CountingVerifier {
|
|
fn verify_server_cert(
|
|
&self,
|
|
end_entity: &CertificateDer<'_>,
|
|
intermediates: &[CertificateDer<'_>],
|
|
server_name: &ServerName<'_>,
|
|
ocsp_response: &[u8],
|
|
now: UnixTime,
|
|
) -> Result<ServerCertVerified, RustlsError> {
|
|
self.counts.certificate_calls.fetch_add(1, Ordering::SeqCst);
|
|
let result = self.inner.verify_server_cert(
|
|
end_entity,
|
|
intermediates,
|
|
server_name,
|
|
ocsp_response,
|
|
now,
|
|
);
|
|
if result.is_ok() {
|
|
self.counts
|
|
.certificate_accepts
|
|
.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
result
|
|
}
|
|
|
|
fn verify_tls12_signature(
|
|
&self,
|
|
message: &[u8],
|
|
cert: &CertificateDer<'_>,
|
|
dss: &DigitallySignedStruct,
|
|
) -> Result<HandshakeSignatureValid, RustlsError> {
|
|
self.counts.tls12_calls.fetch_add(1, Ordering::SeqCst);
|
|
self.inner.verify_tls12_signature(message, cert, dss)
|
|
}
|
|
|
|
fn verify_tls13_signature(
|
|
&self,
|
|
message: &[u8],
|
|
cert: &CertificateDer<'_>,
|
|
dss: &DigitallySignedStruct,
|
|
) -> Result<HandshakeSignatureValid, RustlsError> {
|
|
self.counts.tls13_calls.fetch_add(1, Ordering::SeqCst);
|
|
let result = self.inner.verify_tls13_signature(message, cert, dss);
|
|
if result.is_ok() {
|
|
self.counts.tls13_accepts.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
result
|
|
}
|
|
|
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
|
self.inner.supported_verify_schemes()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
static TEST_VERIFICATION_COUNTERS: LazyLock<Mutex<HashMap<ThreadId, Weak<VerificationCounts>>>> =
|
|
LazyLock::new(|| Mutex::new(HashMap::new()));
|
|
|
|
/// Lexically scopes a verifier decorator to the client provider constructed on
|
|
/// the current test thread. The verifier retains its own counter after QUIC
|
|
/// starts; unrelated parallel test threads continue to use the undecorated
|
|
/// production verifier.
|
|
#[cfg(test)]
|
|
pub(crate) struct TestVerificationCounter {
|
|
thread_id: ThreadId,
|
|
counts: Arc<VerificationCounts>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl TestVerificationCounter {
|
|
pub(crate) fn install() -> eyre::Result<Self> {
|
|
let thread_id = thread::current().id();
|
|
let counts = Arc::new(VerificationCounts::default());
|
|
let mut installed = TEST_VERIFICATION_COUNTERS
|
|
.lock()
|
|
.map_err(|_| eyre::eyre!("TLS verification counter registry was poisoned"))?;
|
|
installed.retain(|_, counter| counter.strong_count() != 0);
|
|
eyre::ensure!(
|
|
!installed.contains_key(&thread_id),
|
|
"TLS verification counter is already installed on this test thread"
|
|
);
|
|
installed.insert(thread_id, Arc::downgrade(&counts));
|
|
drop(installed);
|
|
Ok(Self { thread_id, counts })
|
|
}
|
|
|
|
pub(crate) fn snapshot(&self) -> VerificationSnapshot {
|
|
self.counts.snapshot()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl Drop for TestVerificationCounter {
|
|
fn drop(&mut self) {
|
|
let Ok(mut installed) = TEST_VERIFICATION_COUNTERS.lock() else {
|
|
return;
|
|
};
|
|
let should_remove = installed
|
|
.get(&self.thread_id)
|
|
.and_then(Weak::upgrade)
|
|
.is_some_and(|counts| Arc::ptr_eq(&counts, &self.counts));
|
|
if should_remove {
|
|
installed.remove(&self.thread_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn decorate_verifier_for_current_test(
|
|
verifier: Arc<dyn ServerCertVerifier>,
|
|
) -> Arc<dyn ServerCertVerifier> {
|
|
let counts = TEST_VERIFICATION_COUNTERS
|
|
.lock()
|
|
.ok()
|
|
.and_then(|installed| {
|
|
installed
|
|
.get(&thread::current().id())
|
|
.and_then(Weak::upgrade)
|
|
});
|
|
counts.map_or(verifier.clone(), |counts| {
|
|
Arc::new(CountingVerifier {
|
|
inner: verifier,
|
|
counts,
|
|
})
|
|
})
|
|
}
|
|
|
|
pub(crate) fn sni_for_peer(peer_id: PeerId) -> Result<String, RustlsError> {
|
|
server_name_for_peer(peer_id).map_err(|_| application_verification_failure())
|
|
}
|
|
|
|
fn expected_peer_from_server_name(server_name: &ServerName<'_>) -> Result<PeerId, RustlsError> {
|
|
let ServerName::DnsName(dns_name) = server_name else {
|
|
return Err(application_verification_failure());
|
|
};
|
|
let encoded = dns_name.as_ref();
|
|
let suffix = crate::identity::PEER_SNI_SUFFIX;
|
|
let raw_peer_id = encoded
|
|
.strip_suffix(suffix)
|
|
.ok_or_else(application_verification_failure)?;
|
|
let peer_id = raw_peer_id
|
|
.parse::<PeerId>()
|
|
.map_err(|_| application_verification_failure())?;
|
|
if server_name_for_peer(peer_id).map_err(|_| application_verification_failure())? != encoded {
|
|
return Err(application_verification_failure());
|
|
}
|
|
Ok(peer_id)
|
|
}
|
|
|
|
fn application_verification_failure() -> RustlsError {
|
|
RustlsError::InvalidCertificate(CertificateError::ApplicationVerificationFailure)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use eyre::ensure;
|
|
use lanspread_proto::PeerId;
|
|
use rustls::pki_types::ServerName;
|
|
|
|
use super::{expected_peer_from_server_name, protocol_alpn, sni_for_peer};
|
|
|
|
#[test]
|
|
fn alpn_is_bound_to_the_current_wire_version() {
|
|
assert_eq!(protocol_alpn(), lanspread_proto::ALPN_PROTOCOL);
|
|
assert_ne!(protocol_alpn(), b"lanspread/7");
|
|
}
|
|
|
|
#[test]
|
|
fn expected_peer_round_trips_only_through_exact_sni() -> eyre::Result<()> {
|
|
let peer_id = PeerId::from_bytes([0x5a; 32]);
|
|
let encoded = sni_for_peer(peer_id)?;
|
|
let server_name = ServerName::try_from(encoded)?;
|
|
ensure!(expected_peer_from_server_name(&server_name)? == peer_id);
|
|
|
|
let wrong_suffix = ServerName::try_from(format!("{peer_id}.example.invalid"))?;
|
|
ensure!(expected_peer_from_server_name(&wrong_suffix).is_err());
|
|
Ok(())
|
|
}
|
|
}
|