Skip to main content

irontide_engine/
ssl_manager.rs

1//! SSL certificate management for SSL torrents.
2//!
3//! Handles loading or auto-generating client certificates and building
4//! per-torrent TLS configurations from the CA cert embedded in .torrent files.
5
6use std::sync::Arc;
7
8use irontide_wire::ssl::{self, SslConfig};
9
10use irontide_settings::Settings;
11
12/// Manages SSL certificates for the session.
13///
14/// Created once at session start; provides per-torrent TLS configs on demand.
15pub struct SslManager {
16    /// Our client certificate (PEM bytes).
17    our_cert_pem: Vec<u8>,
18    /// Our private key (PEM bytes).
19    our_key_pem: Vec<u8>,
20}
21
22impl SslManager {
23    /// Create a new SSL manager from session settings.
24    ///
25    /// If `ssl_cert_path` / `ssl_key_path` are set, loads them from disk.
26    /// Otherwise, generates a self-signed cert and optionally persists it
27    /// to `resume_data_dir`.
28    ///
29    /// # Errors
30    /// Returns an error if the certificate or key material is missing or cannot be parsed.
31    pub fn new(settings: &Settings) -> crate::Result<Self> {
32        let (cert_pem, key_pem) = if let (Some(cert_path), Some(key_path)) =
33            (&settings.ssl_cert_path, &settings.ssl_key_path)
34        {
35            let cert = std::fs::read(cert_path).map_err(|e| {
36                crate::Error::Config(format!(
37                    "failed to read SSL cert {}: {e}",
38                    cert_path.display()
39                ))
40            })?;
41            let key = std::fs::read(key_path).map_err(|e| {
42                crate::Error::Config(format!(
43                    "failed to read SSL key {}: {e}",
44                    key_path.display()
45                ))
46            })?;
47            (cert, key)
48        } else {
49            // Auto-generate self-signed cert
50            let (cert, key) = ssl::generate_self_signed_cert()
51                .map_err(|e| crate::Error::Config(format!("SSL cert generation: {e}")))?;
52
53            // Persist to resume_data_dir if available
54            if let Some(ref dir) = settings.resume_data_dir {
55                let cert_path = dir.join("ssl_cert.pem");
56                let key_path = dir.join("ssl_key.pem");
57                // Best-effort persistence -- don't fail if dir doesn't exist yet
58                let _ = std::fs::create_dir_all(dir);
59                let _ = std::fs::write(&cert_path, &cert);
60                let _ = std::fs::write(&key_path, &key);
61            }
62
63            (cert, key)
64        };
65
66        Ok(Self {
67            our_cert_pem: cert_pem,
68            our_key_pem: key_pem,
69        })
70    }
71
72    /// Create an SSL manager from existing PEM bytes.
73    #[must_use]
74    pub fn from_pem(cert_pem: Vec<u8>, key_pem: Vec<u8>) -> Self {
75        Self {
76            our_cert_pem: cert_pem,
77            our_key_pem: key_pem,
78        }
79    }
80
81    /// Build an [`SslConfig`] for a specific torrent (combines our cert with the torrent's CA).
82    #[must_use]
83    pub fn config_for_torrent(&self, ca_cert_pem: &[u8]) -> SslConfig {
84        SslConfig {
85            ca_cert_pem: ca_cert_pem.to_vec(),
86            our_cert_pem: self.our_cert_pem.clone(),
87            our_key_pem: self.our_key_pem.clone(),
88        }
89    }
90
91    /// Build a rustls `ClientConfig` for outbound TLS connections to an SSL torrent.
92    ///
93    /// # Errors
94    /// Returns an error if the rustls client configuration cannot be built.
95    pub fn client_config(
96        &self,
97        ca_cert_pem: &[u8],
98    ) -> Result<Arc<rustls::ClientConfig>, irontide_wire::Error> {
99        let config = self.config_for_torrent(ca_cert_pem);
100        ssl::build_client_config(&config)
101    }
102
103    /// Build a rustls `ServerConfig` for inbound TLS connections to an SSL torrent.
104    ///
105    /// # Errors
106    /// Returns an error if the rustls server configuration cannot be built.
107    pub fn server_config(
108        &self,
109        ca_cert_pem: &[u8],
110    ) -> Result<Arc<rustls::ServerConfig>, irontide_wire::Error> {
111        let config = self.config_for_torrent(ca_cert_pem);
112        ssl::build_server_config(&config)
113    }
114
115    /// Our client certificate in PEM format.
116    #[must_use]
117    pub fn cert_pem(&self) -> &[u8] {
118        &self.our_cert_pem
119    }
120
121    /// Our private key in PEM format.
122    #[must_use]
123    pub fn key_pem(&self) -> &[u8] {
124        &self.our_key_pem
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn ssl_manager_generates_cert() {
134        let settings = Settings::default();
135        let mgr = SslManager::new(&settings).unwrap();
136
137        // Cert should be valid PEM
138        assert!(!mgr.cert_pem().is_empty());
139        assert!(!mgr.key_pem().is_empty());
140        assert!(mgr.cert_pem().starts_with(b"-----BEGIN CERTIFICATE-----"));
141        assert!(
142            mgr.key_pem().starts_with(b"-----BEGIN PRIVATE KEY-----")
143                || mgr
144                    .key_pem()
145                    .starts_with(b"-----BEGIN RSA PRIVATE KEY-----")
146                || mgr.key_pem().starts_with(b"-----BEGIN EC PRIVATE KEY-----")
147        );
148    }
149
150    #[test]
151    fn ssl_manager_builds_config() {
152        let settings = Settings::default();
153        let mgr = SslManager::new(&settings).unwrap();
154
155        // Use the manager's own cert as the CA cert (self-signed acts as its own CA)
156        let ca_cert = mgr.cert_pem().to_vec();
157
158        // Both client and server configs should build successfully
159        let client_cfg = mgr.client_config(&ca_cert).unwrap();
160        assert!(Arc::strong_count(&client_cfg) >= 1);
161
162        let server_cfg = mgr.server_config(&ca_cert).unwrap();
163        assert!(Arc::strong_count(&server_cfg) >= 1);
164
165        // config_for_torrent should produce a valid SslConfig
166        let ssl_config = mgr.config_for_torrent(&ca_cert);
167        assert_eq!(ssl_config.ca_cert_pem, ca_cert);
168        assert_eq!(ssl_config.our_cert_pem, mgr.cert_pem());
169        assert_eq!(ssl_config.our_key_pem, mgr.key_pem());
170    }
171
172    #[test]
173    fn ssl_manager_from_pem_round_trip() {
174        // Generate a cert, then reconstruct via from_pem
175        let settings = Settings::default();
176        let original = SslManager::new(&settings).unwrap();
177
178        let restored =
179            SslManager::from_pem(original.cert_pem().to_vec(), original.key_pem().to_vec());
180
181        assert_eq!(restored.cert_pem(), original.cert_pem());
182        assert_eq!(restored.key_pem(), original.key_pem());
183
184        // The restored manager should also build valid configs
185        let ca_cert = restored.cert_pem().to_vec();
186        restored.client_config(&ca_cert).unwrap();
187        restored.server_config(&ca_cert).unwrap();
188    }
189}