OXYGENHUB
◆ v0.1.0 spectra / crates / spectra-mcp / src / tls.rs
10.2 KiB · Rust · last changed Serve other machines over TLS, trusted through the instance key (#20) 1 day ago source blame history raw
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.
17
pub fn provider() -> Arc<rustls::crypto::CryptoProvider> {
18
    Arc::new(rustls::crypto::ring::default_provider())
19
}
20
21
/// SHA-256 of a certificate's SubjectPublicKeyInfo: what a client pins.
22
pub fn spki_sha256(cert_der: &[u8]) -> Result<[u8; 32]> {
23
    let (_, cert) = x509_parser::parse_x509_certificate(cert_der).map_err(|e| anyhow::anyhow!("not an X.509 certificate: {e}"))?;
24
    let d = ring::digest::digest(&ring::digest::SHA256, cert.public_key().raw);
25
    Ok(d.as_ref().try_into().expect("SHA-256 is 32 bytes"))
26
}
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 {
45
    pub fn load(data: &Path, source: &CertSource) -> Result<ServerTls> {
46
        let (cert_pem, key_pem, names) = match source {
47
            CertSource::Own { names } => {
48
                let keys = data.join("keys");
49
                let (crt, key) = (keys.join("tls.crt"), keys.join("tls.key"));
50
                if !crt.exists() || !key.exists() {
51
                    make_own(&crt, &key, names)?;
52
                }
53
                (std::fs::read_to_string(&crt)?, std::fs::read_to_string(&key)?, names.clone())
54
            }
55
            CertSource::Files { cert, key } => (
56
                std::fs::read_to_string(cert).with_context(|| format!("reading {}", cert.display()))?,
57
                std::fs::read_to_string(key).with_context(|| format!("reading {}", key.display()))?,
58
                vec![],
59
            ),
60
        };
61
        let chain: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_pem.as_bytes()).collect::<Result<_, _>>()?;
62
        let leaf = chain.first().context("no certificate in the PEM")?;
63
        let spki = spki_sha256(leaf)?;
64
        let key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut key_pem.as_bytes())?.context("no private key in the PEM")?;
65
        let mut config = rustls::ServerConfig::builder_with_provider(provider())
66
            .with_safe_default_protocol_versions()?
67
            .with_no_client_auth()
68
            .with_single_cert(chain, key)?;
69
        config.alpn_protocols = vec![b"http/1.1".to_vec()];
70
        Ok(ServerTls { config: Arc::new(config), spki_sha256: spki, names })
71
    }
72
}
73
74
fn make_own(crt: &Path, key: &Path, names: &[String]) -> Result<()> {
75
    if names.is_empty() {
76
        bail!("a certificate needs at least one name ([server] names)");
77
    }
78
    let ck = rcgen::generate_simple_self_signed(names.to_vec())?;
79
    if let Some(dir) = crt.parent() {
80
        std::fs::create_dir_all(dir)?;
81
    }
82
    write_private(key, ck.signing_key.serialize_pem().as_bytes())?;
83
    std::fs::write(crt, ck.cert.pem())?;
84
    Ok(())
85
}
86
87
fn write_private(path: &Path, bytes: &[u8]) -> Result<()> {
88
    #[cfg(unix)]
89
    {
90
        use std::io::Write;
91
        use std::os::unix::fs::OpenOptionsExt;
92
        let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(path)?;
93
        f.write_all(bytes)?;
94
        Ok(())
95
    }
96
    #[cfg(not(unix))]
97
    {
98
        std::fs::write(path, bytes)?;
99
        Ok(())
100
    }
101
}
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 {
111
    pub fn new(tcp: TcpListener, config: Arc<rustls::ServerConfig>) -> Result<TlsListener> {
112
        let addr = tcp.local_addr()?;
113
        let acceptor = tokio_rustls::TlsAcceptor::from(config);
114
        let (tx, rx) = tokio::sync::mpsc::channel(64);
115
        tokio::spawn(async move {
116
            loop {
117
                let (stream, peer) = match tcp.accept().await {
118
                    Ok(x) => x,
119
                    Err(e) => {
120
                        tracing::warn!("accept: {e}");
121
                        tokio::time::sleep(Duration::from_millis(50)).await;
122
                        continue;
123
                    }
124
                };
125
                let (acceptor, out) = (acceptor.clone(), tx.clone());
126
                tokio::spawn(async move {
127
                    match tokio::time::timeout(Duration::from_secs(10), acceptor.accept(stream)).await {
128
                        Ok(Ok(tls)) => {
129
                            let _ = out.send((tls, peer)).await;
130
                        }
131
                        Ok(Err(e)) => tracing::debug!(%peer, "TLS handshake failed: {e}"),
132
                        Err(_) => tracing::debug!(%peer, "TLS handshake timed out"),
133
                    }
134
                });
135
                if tx.is_closed() {
136
                    return;
137
                }
138
            }
139
        });
140
        Ok(TlsListener { rx, addr })
141
    }
142
}
143
144
impl axum::serve::Listener for TlsListener {
145
    type Io = tokio_rustls::server::TlsStream<TcpStream>;
146
    type Addr = SocketAddr;
147
148
    async fn accept(&mut self) -> (Self::Io, Self::Addr) {
149
        match self.rx.recv().await {
150
            Some(x) => x,
151
            // The accept loop never ends while we hold the receiver.
152
            None => std::future::pending().await,
153
        }
154
    }
155
156
    fn local_addr(&self) -> std::io::Result<Self::Addr> {
157
        Ok(self.addr)
158
    }
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 {
173
    pub fn new(pin: Option<[u8; 32]>) -> Arc<Pinned> {
174
        Arc::new(Pinned { pin, seen: Mutex::new(None), algs: provider().signature_verification_algorithms })
175
    }
176
177
    /// The certificate (DER) and its pin from the last handshake.
178
    pub fn seen(&self) -> Option<(Vec<u8>, [u8; 32])> {
179
        self.seen.lock().unwrap().clone()
180
    }
181
182
    pub fn client_config(self: &Arc<Self>) -> Result<Arc<rustls::ClientConfig>> {
183
        let mut c = rustls::ClientConfig::builder_with_provider(provider())
184
            .with_safe_default_protocol_versions()?
185
            .dangerous()
186
            .with_custom_certificate_verifier(self.clone())
187
            .with_no_client_auth();
188
        c.alpn_protocols = vec![b"http/1.1".to_vec()];
189
        Ok(Arc::new(c))
190
    }
191
}
192
193
impl ServerCertVerifier for Pinned {
194
    fn verify_server_cert(
195
        &self,
196
        end_entity: &CertificateDer<'_>,
197
        _intermediates: &[CertificateDer<'_>],
198
        _server_name: &ServerName<'_>,
199
        _ocsp: &[u8],
200
        _now: UnixTime,
201
    ) -> Result<ServerCertVerified, rustls::Error> {
202
        let spki = spki_sha256(end_entity).map_err(|_| rustls::Error::InvalidCertificate(rustls::CertificateError::BadEncoding))?;
203
        *self.seen.lock().unwrap() = Some((end_entity.to_vec(), spki));
204
        match self.pin {
205
            Some(pin) if pin != spki => Err(rustls::Error::InvalidCertificate(rustls::CertificateError::ApplicationVerificationFailure)),
206
            _ => Ok(ServerCertVerified::assertion()),
207
        }
208
    }
209
210
    fn verify_tls12_signature(
211
        &self,
212
        message: &[u8],
213
        cert: &CertificateDer<'_>,
214
        dss: &DigitallySignedStruct,
215
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
216
        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.algs)
217
    }
218
219
    fn verify_tls13_signature(
220
        &self,
221
        message: &[u8],
222
        cert: &CertificateDer<'_>,
223
        dss: &DigitallySignedStruct,
224
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
225
        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.algs)
226
    }
227
228
    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
229
        self.algs.supported_schemes()
230
    }
231
}
232
233
/// Connect to `host:port` over TLS with `config`.
234
pub async fn connect(addr: &str, config: Arc<rustls::ClientConfig>) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
235
    let tcp = tokio::time::timeout(Duration::from_secs(10), TcpStream::connect(addr))
236
        .await
237
        .with_context(|| format!("connecting to {addr}: timed out"))?
238
        .with_context(|| format!("connecting to {addr}"))?;
239
    tcp.set_nodelay(true).ok();
240
    let host = addr.rsplit_once(':').map(|(h, _)| h.trim_start_matches('[').trim_end_matches(']')).unwrap_or(addr);
241
    let name = ServerName::try_from(host.to_string()).unwrap_or_else(|_| ServerName::try_from("spectra").unwrap());
242
    let tls = tokio_rustls::TlsConnector::from(config).connect(name, tcp).await.with_context(|| format!("TLS with {addr}"))?;
243
    Ok(tls)
244
}
245
246
#[cfg(test)]
247
mod tests {
248
    use super::*;
249
250
    #[test]
251
    fn own_certificate_is_made_once_and_pinned() {
252
        let dir = tempfile::tempdir().unwrap();
253
        let src = CertSource::Own { names: vec!["localhost".into()] };
254
        let a = ServerTls::load(dir.path(), &src).unwrap();
255
        let b = ServerTls::load(dir.path(), &src).unwrap();
256
        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;
260
            let mode = std::fs::metadata(dir.path().join("keys/tls.key")).unwrap().permissions().mode();
261
            assert_eq!(mode & 0o777, 0o600);
262
        }
263
    }
264
}