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