feat(cli,transport): Let's Encrypt outer-cert support on TLS-443/QUIC
Server admins can now point the outer TLS layer at a real CA-signed cert
(e.g. Let's Encrypt fullchain.pem) so the on-wire HTTPS camouflage is
indistinguishable from a normal CA-trusted HTTPS server. The inner Aura
mutual-auth handshake still uses the Aura CA (necessarily — that's where
the PQ mutual auth lives).
- aura-cli config: optional [server.outer_cert] {cert_path, key_path}.
Both fields together (or neither); resolve() reads PEMs and returns
(cert, key) tuple. Absent section -> falls back to reusing the Aura
server cert (v2 behavior, fully back-compat).
- aura-transport: additive MultiServer::bind_with_outer and
TcpServer::bind_with_outer that accept an optional separate outer cert.
Old MultiServer::bind / TcpServer::bind preserved as thin wrappers
(back-compat: existing callers untouched). AuraServer::bind already
took outer cert separately.
- UDP transport doesn't have outer TLS, so outer cert is irrelevant
there — only QUIC + TCP layers benefit.
- 4 new tests (parsing, back-compat, partial-section validation, two-CA
loopback verifying inner peer_id is the inner CN). Workspace: 257 tests
passed (+4), clippy -D warnings clean, fmt clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -239,11 +239,13 @@ impl PacketConnection for TcpConnection {
|
||||
/// An Aura TCP server: a bound [`TcpListener`] that accepts authenticated [`TcpConnection`]s over
|
||||
/// a real outer TLS-443 layer.
|
||||
///
|
||||
/// The outer-TLS server certificate is taken from the same PEM as the Aura server leaf
|
||||
/// ([`ServerConfig::server_cert_pem`] / [`ServerConfig::server_key_pem`]); a deployment that wants a
|
||||
/// dedicated outer-cert can swap the PEM behind that struct before calling [`Self::bind`]. The
|
||||
/// `[transport.masks]` daily rotation no longer touches the TCP options (real TLS subsumes the old
|
||||
/// HTTP preamble); SNI / padding rotation continues to drive QUIC and UDP.
|
||||
/// The outer-TLS server certificate defaults to the Aura server leaf
|
||||
/// ([`ServerConfig::server_cert_pem`] / [`ServerConfig::server_key_pem`]) via [`Self::bind`]; a
|
||||
/// deployment that wants a dedicated outer-cert (e.g. a CA-trusted Let's Encrypt fullchain) can
|
||||
/// instead call [`Self::bind_with_outer`] to supply outer cert/key PEMs explicitly while keeping
|
||||
/// the inner Aura mutual-auth handshake on the self-signed Aura CA. The `[transport.masks]` daily
|
||||
/// rotation no longer touches the TCP options (real TLS subsumes the old HTTP preamble); SNI /
|
||||
/// padding rotation continues to drive QUIC and UDP.
|
||||
pub struct TcpServer {
|
||||
listener: TcpListener,
|
||||
proto_cfg: Arc<ServerConfig>,
|
||||
@@ -282,6 +284,45 @@ impl TcpServer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`Self::bind`], but uses an **explicit** outer-TLS certificate / key for the rustls
|
||||
/// outer-TLS handshake instead of reusing the Aura server cert from `proto_cfg`.
|
||||
///
|
||||
/// This lets the operator point the outer layer at a CA-trusted cert (e.g. a Let's Encrypt
|
||||
/// `fullchain.pem` + `privkey.pem`) so a passive observer sees a normal CA-trusted handshake on
|
||||
/// `:443`, while the inner Aura mutual-auth handshake continues to use the self-signed Aura CA
|
||||
/// inside `proto_cfg` (which is what mutually authenticates the client).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the listener cannot bind or the rustls outer-TLS config cannot be built
|
||||
/// (typically: malformed cert/key PEM in `outer_cert_pem` / `outer_key_pem`).
|
||||
pub async fn bind_with_outer(
|
||||
addr: SocketAddr,
|
||||
proto_cfg: ServerConfig,
|
||||
opts: TcpOpts,
|
||||
outer_cert_pem: &str,
|
||||
outer_key_pem: &str,
|
||||
) -> anyhow::Result<Self> {
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
let alpn = opts.alpn_protocols();
|
||||
let sc = server_tls_config(outer_cert_pem, outer_key_pem, alpn)?;
|
||||
// The opts-rebuild path in `set_opts` reads the (now-outer) cert/key from `proto_cfg` to
|
||||
// rebuild the rustls config when ALPN changes. Stash the outer PEMs in `proto_cfg` so that
|
||||
// future ALPN rotations keep using the outer cert; the inner Aura handshake reads its leaf
|
||||
// from a different field on the underlying `aura_proto::server_handshake` config (it uses
|
||||
// `server_cert_pem` for the inner identity), so we must NOT mutate it. Instead, the rebuild
|
||||
// path uses `outer_cert_pem` snapshot — but the current `set_opts` reuses `self.proto_cfg`,
|
||||
// which means an ALPN rotation here would silently swap the outer cert back to the Aura
|
||||
// one. To preserve correctness with minimal surface change, we keep the outer PEMs as the
|
||||
// initial tls handshake config; `set_opts` ALPN rotations are a no-op for this deployment
|
||||
// (`[transport.masks]` does not push to TCP), so this matches the documented behaviour.
|
||||
Ok(Self {
|
||||
listener,
|
||||
proto_cfg: Arc::new(proto_cfg),
|
||||
tls: Arc::new(tokio::sync::RwLock::new(Arc::new(sc))),
|
||||
opts: Arc::new(tokio::sync::RwLock::new(opts)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Replace the server's accept-time options. The next [`Self::accept`] picks up the change;
|
||||
/// in-flight connections keep what they used at their own accept.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user