OXYGENHUB

Coverage / crates/spectra-mcp/src/tls.rs 80.4%

131 of 163 lines run, 26 of 61 functions · run #18 on main · run missed

1
//! TLS for client and server (spec §8.6). The server makes a certificate
2
//! of its own; the instance key vouches for it with an `instance.tls`
3
//! event; a client trusts that one certificate, pinned by the SHA-256 of
4
//! its SubjectPublicKeyInfo, and never a certificate authority.
5
6
use anyhow::{bail, Context, Result};
7
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
8
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
9
use rustls::{DigitallySignedStruct, SignatureScheme};
10
use std::net::SocketAddr;
11
use std::path::{Path, PathBuf};
12
use std::sync::{Arc, Mutex};
13
use std::time::Duration;
14
use tokio::net::{TcpListener, TcpStream};
15
16
/// The crypto provider: ring, as OxygenHub's.
1717×
pub fn provider() -> Arc<rustls::crypto::CryptoProvider> {
1817×
    Arc::new(rustls::crypto::ring::default_provider())
1917×
}
20
21
/// SHA-256 of a certificate's SubjectPublicKeyInfo: what a client pins.
2211×
pub fn spki_sha256(cert_der: &[u8]) -> Result<[u8; 32]> {
2311×
    let (_, cert) = x509_parser::parse_x509_certificate(cert_der).map_err(|e| anyhow::anyhow!("not an X.509 certificate: {e}"))?;
2411×
    let d = ring::digest::digest(&ring::digest::SHA256, cert.public_key().raw);
2511×
    Ok(d.as_ref().try_into().expect("SHA-256 is 32 bytes"))
2611×
}
27
28
/// Where a server's certificate comes from (`[server] tls`).
29
#[derive(Clone, Debug)]
30
pub enum CertSource {
31
    /// Made on first start in `<data>/keys/tls.{key,crt}`, naming these names.
32
    Own { names: Vec<String> },
33
    /// Files from elsewhere (PEM).
34
    Files { cert: PathBuf, key: PathBuf },
35
}
36
37
/// A server's TLS: its rustls configuration and the certificate's pin.
38
pub struct ServerTls {
39
    pub config: Arc<rustls::ServerConfig>,
40
    pub spki_sha256: [u8; 32],
41
    pub names: Vec<String>,
42
}
43
44
impl ServerTls {
455×
    pub fn load(data: &Path, source: &CertSource) -> Result<ServerTls> {
465×
        let (cert_pem, key_pem, names) = match source {
475×
            CertSource::Own { names } => {
485×
                let keys = data.join("keys");
495×
                let (crt, key) = (keys.join("tls.crt"), keys.join("tls.key"));
505×
                if !crt.exists() || !key.exists() {
513×
                    make_own(&crt, &key, names)?;
522×
                }
535×
                (std::fs::read_to_string(&crt)?, std::fs::read_to_string(&key)?, names.clone())
54
            }
550×
            CertSource::Files { cert, key } => (
560×
                std::fs::read_to_string(cert).with_context(|| format!("reading {}", cert.display()))?,
570×
                std::fs::read_to_string(key).with_context(|| format!("reading {}", key.display()))?,
580×
                vec![],
59
            ),
60
        };
615×
        let chain: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_pem.as_bytes()).collect::<Result<_, _>>()?;
625×
        let leaf = chain.first().context("no certificate in the PEM")?;
635×
        let spki = spki_sha256(leaf)?;
645×
        let key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut key_pem.as_bytes())?.context("no private key in the PEM")?;
655×
        let mut config = rustls::ServerConfig::builder_with_provider(provider())
665×
            .with_safe_default_protocol_versions()?
675×
            .with_no_client_auth()
685×
            .with_single_cert(chain, key)?;
695×
        config.alpn_protocols = vec![b"http/1.1".to_vec()];
705×
        Ok(ServerTls { config: Arc::new(config), spki_sha256: spki, names })
715×
    }
72
}
73
743×
fn make_own(crt: &Path, key: &Path, names: &[String]) -> Result<()> {
753×
    if names.is_empty() {
760×
        bail!("a certificate needs at least one name ([server] names)");
773×
    }
783×
    let ck = rcgen::generate_simple_self_signed(names.to_vec())?;
793×
    if let Some(dir) = crt.parent() {
803×
        std::fs::create_dir_all(dir)?;
810×
    }
823×
    write_private(key, ck.signing_key.serialize_pem().as_bytes())?;
833×
    std::fs::write(crt, ck.cert.pem())?;
843×
    Ok(())
853×
}
86
873×
fn write_private(path: &Path, bytes: &[u8]) -> Result<()> {
88
    #[cfg(unix)]
89
    {
90
        use std::io::Write;
91
        use std::os::unix::fs::OpenOptionsExt;
923×
        let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(path)?;
933×
        f.write_all(bytes)?;
943×
        Ok(())
95
    }
96
    #[cfg(not(unix))]
97
    {
98
        std::fs::write(path, bytes)?;
99
        Ok(())
100
    }
1013×
}
102
103
/// A TCP listener that hands axum connections once their TLS handshake is
104
/// done; handshakes run concurrently, so a slow one holds up nobody.
105
pub struct TlsListener {
106
    rx: tokio::sync::mpsc::Receiver<(tokio_rustls::server::TlsStream<TcpStream>, SocketAddr)>,
107
    addr: SocketAddr,
108
}
109
110
impl TlsListener {
1113×
    pub fn new(tcp: TcpListener, config: Arc<rustls::ServerConfig>) -> Result<TlsListener> {
1123×
        let addr = tcp.local_addr()?;
1133×
        let acceptor = tokio_rustls::TlsAcceptor::from(config);
1143×
        let (tx, rx) = tokio::sync::mpsc::channel(64);
1153×
        tokio::spawn(async move {
116
            loop {
1179×
                let (stream, peer) = match tcp.accept().await {
1186×
                    Ok(x) => x,
1190×
                    Err(e) => {
1200×
                        tracing::warn!("accept: {e}");
1210×
                        tokio::time::sleep(Duration::from_millis(50)).await;
1220×
                        continue;
123
                    }
124
                };
1256×
                let (acceptor, out) = (acceptor.clone(), tx.clone());
1266×
                tokio::spawn(async move {
1276×
                    match tokio::time::timeout(Duration::from_secs(10), acceptor.accept(stream)).await {
1285×
                        Ok(Ok(tls)) => {
1295×
                            let _ = out.send((tls, peer)).await;
130
                        }
1311×
                        Ok(Err(e)) => tracing::debug!(%peer, "TLS handshake failed: {e}"),
1320×
                        Err(_) => tracing::debug!(%peer, "TLS handshake timed out"),
133
                    }
1346×
                });
1356×
                if tx.is_closed() {
1360×
                    return;
1376×
                }
138
            }
1390×
        });
1403×
        Ok(TlsListener { rx, addr })
1413×
    }
142
}
143
144
impl axum::serve::Listener for TlsListener {
145
    type Io = tokio_rustls::server::TlsStream<TcpStream>;
146
    type Addr = SocketAddr;
147
1488×
    async fn accept(&mut self) -> (Self::Io, Self::Addr) {
1498×
        match self.rx.recv().await {
1505×
            Some(x) => x,
151
            // The accept loop never ends while we hold the receiver.
1520×
            None => std::future::pending().await,
153
        }
1545×
    }
155
1560×
    fn local_addr(&self) -> std::io::Result<Self::Addr> {
1570×
        Ok(self.addr)
1580×
    }
159
}
160
161
/// A server certificate verifier that accepts exactly one public key, or,
162
/// with no pin, any key, remembering it for the caller to check
163
/// (`spectra connect`). The handshake's signature is always checked, so
164
/// the server must hold the certificate's private key.
165
#[derive(Debug)]
166
pub struct Pinned {
167
    pin: Option<[u8; 32]>,
168
    seen: Mutex<Option<(Vec<u8>, [u8; 32])>>,
169
    algs: rustls::crypto::WebPkiSupportedAlgorithms,
170
}
171
172
impl Pinned {
1736×
    pub fn new(pin: Option<[u8; 32]>) -> Arc<Pinned> {
1746×
        Arc::new(Pinned { pin, seen: Mutex::new(None), algs: provider().signature_verification_algorithms })
1756×
    }
176
177
    /// The certificate (DER) and its pin from the last handshake.
1783×
    pub fn seen(&self) -> Option<(Vec<u8>, [u8; 32])> {
1793×
        self.seen.lock().unwrap().clone()
1803×
    }
181
1826×
    pub fn client_config(self: &Arc<Self>) -> Result<Arc<rustls::ClientConfig>> {
1836×
        let mut c = rustls::ClientConfig::builder_with_provider(provider())
1846×
            .with_safe_default_protocol_versions()?
1856×
            .dangerous()
1866×
            .with_custom_certificate_verifier(self.clone())
1876×
            .with_no_client_auth();
1886×
        c.alpn_protocols = vec![b"http/1.1".to_vec()];
1896×
        Ok(Arc::new(c))
1906×
    }
191
}
192
193
impl ServerCertVerifier for Pinned {
1946×
    fn verify_server_cert(
1956×
        &self,
1966×
        end_entity: &CertificateDer<'_>,
1976×
        _intermediates: &[CertificateDer<'_>],
1986×
        _server_name: &ServerName<'_>,
1996×
        _ocsp: &[u8],
2006×
        _now: UnixTime,
2016×
    ) -> Result<ServerCertVerified, rustls::Error> {
2026×
        let spki = spki_sha256(end_entity).map_err(|_| rustls::Error::InvalidCertificate(rustls::CertificateError::BadEncoding))?;
2036×
        *self.seen.lock().unwrap() = Some((end_entity.to_vec(), spki));
2043×
        match self.pin {
2053×
            Some(pin) if pin != spki => Err(rustls::Error::InvalidCertificate(rustls::CertificateError::ApplicationVerificationFailure)),
2065×
            _ => Ok(ServerCertVerified::assertion()),
207
        }
2086×
    }
209
2100×
    fn verify_tls12_signature(
2110×
        &self,
2120×
        message: &[u8],
2130×
        cert: &CertificateDer<'_>,
2140×
        dss: &DigitallySignedStruct,
2150×
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
2160×
        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.algs)
2170×
    }
218
2195×
    fn verify_tls13_signature(
2205×
        &self,
2215×
        message: &[u8],
2225×
        cert: &CertificateDer<'_>,
2235×
        dss: &DigitallySignedStruct,
2245×
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
2255×
        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.algs)
2265×
    }
227
2286×
    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
2296×
        self.algs.supported_schemes()
2306×
    }
231
}
232
233
/// Connect to `host:port` over TLS with `config`.
2346×
pub async fn connect(addr: &str, config: Arc<rustls::ClientConfig>) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
2356×
    let tcp = tokio::time::timeout(Duration::from_secs(10), TcpStream::connect(addr))
2366×
        .await
2376×
        .with_context(|| format!("connecting to {addr}: timed out"))?
2386×
        .with_context(|| format!("connecting to {addr}"))?;
2396×
    tcp.set_nodelay(true).ok();
2406×
    let host = addr.rsplit_once(':').map(|(h, _)| h.trim_start_matches('[').trim_end_matches(']')).unwrap_or(addr);
2416×
    let name = ServerName::try_from(host.to_string()).unwrap_or_else(|_| ServerName::try_from("spectra").unwrap());
2426×
    let tls = tokio_rustls::TlsConnector::from(config).connect(name, tcp).await.with_context(|| format!("TLS with {addr}"))?;
2435×
    Ok(tls)
2446×
}
245
246
#[cfg(test)]
247
mod tests {
248
    use super::*;
249
250
    #[test]
2511×
    fn own_certificate_is_made_once_and_pinned() {
2521×
        let dir = tempfile::tempdir().unwrap();
2531×
        let src = CertSource::Own { names: vec!["localhost".into()] };
2541×
        let a = ServerTls::load(dir.path(), &src).unwrap();
2551×
        let b = ServerTls::load(dir.path(), &src).unwrap();
2561×
        assert_eq!(a.spki_sha256, b.spki_sha256, "the certificate is kept between starts");
257
        #[cfg(unix)]
258
        {
259
            use std::os::unix::fs::PermissionsExt;
2601×
            let mode = std::fs::metadata(dir.path().join("keys/tls.key")).unwrap().permissions().mode();
2611×
            assert_eq!(mode & 0o777, 0o600);
262
        }
2631×
    }
264
}