clawsync-transport: SealedFile/SealedFileAck variants + mTLS PEM constructors
Adds the wire surface and the cert-loading constructors that
omni-sync's QuicTransport needs for sealed-day replication to a
peer Garage node.
Protocol additions (rkyv discriminants are positional — appended
at the end, never inserted):
- `SyncMessage::SealedFile { peer_name, payload }`: sealed `.h5`
delivery from `omni-sync` clients (omni-cortex daemons
replicating sealed-day Working / Episodic / Semantic tier
files to a peer).
- `SyncMessage::SealedFileAck { peer_name, bytes, blake3_hex }`:
receiver-computed BLAKE3 acknowledgement so the sender can
verify the bytes landed intact.
QuicConfig constructors for production mTLS:
- `client_with_pem(client_cert, client_key, server_ca)` — loads
the operator's mTLS leaf identity + the CA whose-issued server
certs the client trusts. Builds a real rustls `ClientConfig`
via `with_client_auth_cert`. Server config field gets a
placeholder (never consumed by `quic_connect`).
- `server_with_pem(server_cert, server_key, client_ca)` — mirror
for the receive side. Real rustls `ServerConfig` with
`WebPkiClientVerifier::builder(roots).build()` +
`with_client_cert_verifier` (mutual auth: clients without a
CA-issued cert fail the handshake) + `with_single_cert`.
Validated by `omni-sync`'s D193 production-mTLS round-trip test:
rcgen-generates CA + matching server cert + matching client
cert, writes PEMs to a tempdir, spawns a `QuicServer` with
`server_with_pem`, drives a real `QuicTransport` production-mode
client through `SealedFile` → `SealedFileAck` with mutual cert
verification.
Also includes a `rustls-pemfile = "2.2"` dep + ambient
`cargo fmt` line-collapsing in `framed.rs` and `peer.rs` from
when the workspace was reformatted.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
@@ -104,6 +104,199 @@ impl QuicConfig {
|
||||
client_config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a **client-only** config from PEM paths for production
|
||||
/// mTLS deployments.
|
||||
///
|
||||
/// - `client_cert_pem` / `client_key_pem` form the mTLS leaf
|
||||
/// identity this client presents.
|
||||
/// - `server_ca_pem` is the CA whose-issued server certs the
|
||||
/// client trusts.
|
||||
///
|
||||
/// `server_config` is populated with a placeholder self-signed
|
||||
/// cert so the field is non-`Option`, matching the existing
|
||||
/// constructors. Callers using this constructor are inherently
|
||||
/// client-only (e.g. `omni-sync::QuicTransport`); the
|
||||
/// `server_config` is never consumed by `quic_connect`.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`TransportError::Tls`] for any PEM parse / chain
|
||||
/// construction failure.
|
||||
/// - [`TransportError::Io`] for any I/O failure reading the
|
||||
/// PEM files.
|
||||
pub fn client_with_pem(
|
||||
client_cert_pem: &std::path::Path,
|
||||
client_key_pem: &std::path::Path,
|
||||
server_ca_pem: &std::path::Path,
|
||||
) -> Result<Self, TransportError> {
|
||||
use std::io::BufReader;
|
||||
|
||||
// Load the client mTLS identity.
|
||||
let cert_file = std::fs::File::open(client_cert_pem).map_err(TransportError::Io)?;
|
||||
let mut cert_reader = BufReader::new(cert_file);
|
||||
let cert_chain: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
|
||||
.collect::<std::io::Result<Vec<_>>>()
|
||||
.map_err(TransportError::Io)?;
|
||||
if cert_chain.is_empty() {
|
||||
return Err(TransportError::Tls(format!(
|
||||
"no certificates in {}",
|
||||
client_cert_pem.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let key_file = std::fs::File::open(client_key_pem).map_err(TransportError::Io)?;
|
||||
let mut key_reader = BufReader::new(key_file);
|
||||
let key_der: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut key_reader)
|
||||
.map_err(TransportError::Io)?
|
||||
.ok_or_else(|| {
|
||||
TransportError::Tls(format!(
|
||||
"no private key in {}",
|
||||
client_key_pem.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
// Trust anchor for verifying the server's cert.
|
||||
let ca_file = std::fs::File::open(server_ca_pem).map_err(TransportError::Io)?;
|
||||
let mut ca_reader = BufReader::new(ca_file);
|
||||
let ca_certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut ca_reader)
|
||||
.collect::<std::io::Result<Vec<_>>>()
|
||||
.map_err(TransportError::Io)?;
|
||||
if ca_certs.is_empty() {
|
||||
return Err(TransportError::Tls(format!(
|
||||
"no CA certs in {}",
|
||||
server_ca_pem.display()
|
||||
)));
|
||||
}
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
for c in ca_certs {
|
||||
roots
|
||||
.add(c)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
}
|
||||
|
||||
let tls_client = rustls::ClientConfig::builder()
|
||||
.with_root_certificates(Arc::new(roots))
|
||||
.with_client_auth_cert(cert_chain, key_der)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
let client_config = ClientConfig::new(Arc::new(
|
||||
quinn::crypto::rustls::QuicClientConfig::try_from(tls_client)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?,
|
||||
));
|
||||
|
||||
// Placeholder server_config — never consumed by quic_connect.
|
||||
let placeholder_cert =
|
||||
rcgen::generate_simple_self_signed(vec!["localhost".to_string()])
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
let placeholder_cert_der = CertificateDer::from(placeholder_cert.cert.der().to_vec());
|
||||
let placeholder_key_der =
|
||||
PrivateKeyDer::try_from(placeholder_cert.key_pair.serialize_der())
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
let server_config = ServerConfig::with_single_cert(
|
||||
vec![placeholder_cert_der],
|
||||
placeholder_key_der,
|
||||
)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
|
||||
Ok(Self {
|
||||
server_config,
|
||||
client_config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a **server-only** config from PEM paths for production
|
||||
/// mTLS deployments. The mirror of [`Self::client_with_pem`].
|
||||
///
|
||||
/// - `server_cert_pem` / `server_key_pem` form the mTLS leaf
|
||||
/// identity this server presents.
|
||||
/// - `client_ca_pem` is the CA whose-issued client certs the
|
||||
/// server requires + trusts. Client auth is enforced
|
||||
/// (`with_client_cert_verifier`); a connecting client without
|
||||
/// a matching cert will fail the handshake.
|
||||
///
|
||||
/// `client_config` is populated with a placeholder
|
||||
/// (root-empty, no auth) so the field is non-`Option`. Callers
|
||||
/// using this constructor are inherently server-only.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`TransportError::Tls`] for any PEM parse / chain
|
||||
/// construction failure.
|
||||
/// - [`TransportError::Io`] for any I/O failure.
|
||||
pub fn server_with_pem(
|
||||
server_cert_pem: &std::path::Path,
|
||||
server_key_pem: &std::path::Path,
|
||||
client_ca_pem: &std::path::Path,
|
||||
) -> Result<Self, TransportError> {
|
||||
use std::io::BufReader;
|
||||
|
||||
let cert_file = std::fs::File::open(server_cert_pem).map_err(TransportError::Io)?;
|
||||
let mut cert_reader = BufReader::new(cert_file);
|
||||
let cert_chain: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
|
||||
.collect::<std::io::Result<Vec<_>>>()
|
||||
.map_err(TransportError::Io)?;
|
||||
if cert_chain.is_empty() {
|
||||
return Err(TransportError::Tls(format!(
|
||||
"no certificates in {}",
|
||||
server_cert_pem.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let key_file = std::fs::File::open(server_key_pem).map_err(TransportError::Io)?;
|
||||
let mut key_reader = BufReader::new(key_file);
|
||||
let key_der: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut key_reader)
|
||||
.map_err(TransportError::Io)?
|
||||
.ok_or_else(|| {
|
||||
TransportError::Tls(format!(
|
||||
"no private key in {}",
|
||||
server_key_pem.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
let ca_file = std::fs::File::open(client_ca_pem).map_err(TransportError::Io)?;
|
||||
let mut ca_reader = BufReader::new(ca_file);
|
||||
let ca_certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut ca_reader)
|
||||
.collect::<std::io::Result<Vec<_>>>()
|
||||
.map_err(TransportError::Io)?;
|
||||
if ca_certs.is_empty() {
|
||||
return Err(TransportError::Tls(format!(
|
||||
"no CA certs in {}",
|
||||
client_ca_pem.display()
|
||||
)));
|
||||
}
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
for c in ca_certs {
|
||||
roots
|
||||
.add(c)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
}
|
||||
let client_verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
|
||||
.build()
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
let tls_server = rustls::ServerConfig::builder()
|
||||
.with_client_cert_verifier(client_verifier)
|
||||
.with_single_cert(cert_chain, key_der)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?;
|
||||
let server_config = ServerConfig::with_crypto(Arc::new(
|
||||
quinn::crypto::rustls::QuicServerConfig::try_from(tls_server)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?,
|
||||
));
|
||||
|
||||
// Placeholder client_config. Use insecure NoopVerifier
|
||||
// since this constructor is server-only — client_config is
|
||||
// never consumed.
|
||||
let tls_client = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoopVerifier))
|
||||
.with_no_client_auth();
|
||||
let client_config = ClientConfig::new(Arc::new(
|
||||
quinn::crypto::rustls::QuicClientConfig::try_from(tls_client)
|
||||
.map_err(|e| TransportError::Tls(e.to_string()))?,
|
||||
));
|
||||
|
||||
Ok(Self {
|
||||
server_config,
|
||||
client_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A TLS certificate verifier that accepts any certificate.
|
||||
|
||||
Reference in New Issue
Block a user