1use clap::ArgMatches;
2use log::{info, warn};
3use std::error::Error;
4use std::fs;
5use std::net::IpAddr;
6use std::path::{Path, PathBuf};
7use tonic::transport::{Certificate, ClientTlsConfig, Identity};
8use x509_parser::prelude::{GeneralName, Pem, X509Certificate};
9
10const CERTIFICATE_LABEL: &str = "CERTIFICATE";
12
13enum NameSource {
15 Flag,
17 Address,
19 Pinned,
21}
22
23impl NameSource {
24 fn as_str(&self) -> &'static str {
25 match self {
26 NameSource::Flag => "--tls_domain",
27 NameSource::Address => "-a",
28 NameSource::Pinned => "certificate SAN",
29 }
30 }
31}
32
33pub struct TlsOptions<'a> {
35 cert_path: Option<&'a str>,
37 system_roots: bool,
39 domain: Option<&'a str>,
41}
42
43impl<'a> TlsOptions<'a> {
44 pub fn from_args(args: &'a ArgMatches) -> Self {
46 Self {
47 cert_path: args.get_one::<String>("tls").map(String::as_str),
48 system_roots: args.get_flag("tls_system"),
49 domain: args.get_one::<String>("tls_domain").map(String::as_str),
50 }
51 }
52
53 pub fn is_enabled(&self) -> bool {
55 self.cert_path.is_some() || self.system_roots
56 }
57
58 pub fn client_config(&self, address: &str) -> Result<ClientTlsConfig, Box<dyn Error>> {
69 let Some(cert_path) = self.cert_path else {
70 let (server_name, source) = server_name(&[], address, self.domain);
72 info!(
73 "[TLS] Trusting the system trust store, authenticating orchestrator as '{server_name}' (from {})",
74 source.as_str()
75 );
76
77 return Ok(ClientTlsConfig::new()
78 .with_native_roots()
79 .domain_name(server_name));
80 };
81
82 client_config(cert_path, address, self.domain)
84 }
85}
86
87fn client_config(
100 cert_path: &str,
101 address: &str,
102 tls_domain: Option<&str>,
103) -> Result<ClientTlsConfig, Box<dyn Error>> {
104 let pem = fs::read(cert_path)
105 .map_err(|e| format!("Unable to read certificate at {cert_path}: {e}"))?;
106 let certificates = certificates(&pem, cert_path)?;
107
108 let (server_name, source) = server_name(&certificates, address, tls_domain);
109
110 info!(
111 "[TLS] Trusting {} certificate(s) from {cert_path}, authenticating orchestrator as '{server_name}' (from {})",
112 certificates.len(),
113 source.as_str()
114 );
115
116 if matches!(source, NameSource::Address) && server_name.parse::<IpAddr>().is_ok() {
118 warn!("[TLS] Authenticating an IP address requires an IP Subject Alternative Name.");
119 }
120
121 Ok(ClientTlsConfig::new()
122 .ca_certificate(Certificate::from_pem(pem))
123 .domain_name(server_name))
124}
125
126pub fn server_identity(cert_path: &str, key_path: Option<&str>) -> Identity {
140 let key_path = key_path.map_or_else(|| default_key_path(cert_path), PathBuf::from);
141
142 let cert = fs::read(cert_path)
143 .unwrap_or_else(|e| panic!("Unable to read certificate at {cert_path}: {e}"));
144 let key = fs::read(&key_path)
145 .unwrap_or_else(|e| panic!("Unable to read private key at {}: {e}", key_path.display()));
146
147 info!(
148 "[TLS] Using certificate {cert_path} and private key {}",
149 key_path.display()
150 );
151
152 match certificates(&cert, cert_path) {
154 Ok(certificates) => describe_served_chain(&certificates, cert_path),
155 Err(e) => warn!("[TLS] {e}"),
156 }
157
158 Identity::from_pem(cert, key)
159}
160
161fn default_key_path(cert_path: &str) -> PathBuf {
163 Path::new(cert_path).with_extension("key")
164}
165
166fn certificates(pem: &[u8], cert_path: &str) -> Result<Vec<Pem>, Box<dyn Error>> {
171 let certificates: Vec<Pem> = Pem::iter_from_buffer(pem)
172 .filter_map(Result::ok)
173 .filter(|pem| pem.label == CERTIFICATE_LABEL)
174 .collect();
175
176 if certificates.is_empty() {
177 return Err(format!("No PEM certificate found in {cert_path}.").into());
178 }
179
180 Ok(certificates)
181}
182
183fn server_name(
191 certificates: &[Pem],
192 address: &str,
193 tls_domain: Option<&str>,
194) -> (String, NameSource) {
195 if let Some(domain) = tls_domain {
196 return (domain.to_owned(), NameSource::Flag);
197 }
198
199 let host = host_of(address);
200 if host.parse::<IpAddr>().is_err() {
201 return (host.to_owned(), NameSource::Address);
202 }
203
204 pinned_self_signed_name(certificates).map_or_else(
205 || (host.to_owned(), NameSource::Address),
206 |name| (name, NameSource::Pinned),
207 )
208}
209
210fn host_of(address: &str) -> &str {
212 if let Some(rest) = address.strip_prefix('[') {
214 return rest.split_once(']').map_or(rest, |(host, _)| host);
215 }
216
217 address.rsplit_once(':').map_or(address, |(host, _)| host)
218}
219
220fn pinned_self_signed_name(certificates: &[Pem]) -> Option<String> {
222 let [pinned] = certificates else {
224 return None;
225 };
226
227 let certificate = pinned.parse_x509().ok()?;
228 if certificate.subject().as_raw() != certificate.issuer().as_raw() {
229 return None;
231 }
232
233 subject_alternative_names(&certificate).into_iter().next()
234}
235
236fn subject_alternative_names(certificate: &X509Certificate) -> Vec<String> {
238 let names = certificate
239 .subject_alternative_name()
240 .ok()
241 .flatten()
242 .map(|san| san.value.general_names.as_slice())
243 .unwrap_or_default();
244
245 let dns = names.iter().filter_map(|name| match name {
246 GeneralName::DNSName(dns) => Some((*dns).to_owned()),
247 _ => None,
248 });
249 let ip = names.iter().filter_map(|name| match name {
250 GeneralName::IPAddress(bytes) => to_ip(bytes).map(|ip| ip.to_string()),
251 _ => None,
252 });
253
254 dns.chain(ip).collect()
255}
256
257fn describe_served_chain(certificates: &[Pem], cert_path: &str) {
259 let Ok(certificate) = certificates[0].parse_x509() else {
261 warn!("[TLS] Unable to parse the certificate at {cert_path}");
262 return;
263 };
264
265 let names = subject_alternative_names(&certificate);
267 if names.is_empty() {
268 warn!(
269 "[TLS] The certificate at {cert_path} has no Subject Alternative Name (SAN), \
270 regenerate it with e.g. -addext \"subjectAltName=DNS:orchestrator.example.com\""
271 );
272 } else {
273 info!("[TLS] Serving a certificate valid for {}", names.join(", "));
274 }
275
276 let is_self_signed = certificate.subject().as_raw() == certificate.issuer().as_raw();
277 if is_self_signed {
278 info!("[TLS] The certificate is self-signed, clients must trust it directly (--tls)");
279 } else if certificates.len() == 1 {
280 info!(
282 "[TLS] The certificate was issued by '{}', clients must trust that CA (--tls); \
283 if it is an intermediate CA, append its certificate to {cert_path} (a full chain, \
284 leaf first), or clients will reject the connection with 'UnknownIssuer'",
285 certificate.issuer()
286 );
287 } else {
288 info!(
289 "[TLS] Serving a chain of {} certificates, issued by '{}'",
290 certificates.len(),
291 certificate.issuer()
292 );
293 }
294}
295
296fn to_ip(bytes: &[u8]) -> Option<IpAddr> {
298 match bytes.len() {
299 4 => Some(IpAddr::from(<[u8; 4]>::try_from(bytes).ok()?)),
300 16 => Some(IpAddr::from(<[u8; 16]>::try_from(bytes).ok()?)),
301 _ => None,
302 }
303}