1use anyhow::{Context, Result};
2use log::debug;
3use std::path::Path;
4use tonic::transport::{Certificate, ClientTlsConfig, Identity};
5use x509_certificate::X509Certificate;
6
7const CA_RAW: &[u8] = include_str!("../.resources/tls/ca.pem").as_bytes();
8const NOBODY_CRT: &[u8] = include_str!(env!("GL_NOBODY_CRT")).as_bytes();
9const NOBODY_KEY: &[u8] = include_str!(env!("GL_NOBODY_KEY")).as_bytes();
10
11#[derive(Clone, Debug)]
19pub struct TlsConfig {
20 pub(crate) inner: ClientTlsConfig,
21
22 pub(crate) private_key: Option<Vec<u8>>,
25
26 pub ca: Vec<u8>,
27
28 pub x509_cert: Option<X509Certificate>,
32}
33
34fn load_file_or_default(varname: &str, default: &[u8]) -> Vec<u8> {
38 match std::env::var(varname) {
39 Ok(fname) => {
40 debug!("Loading file {} for envvar {}", fname, varname);
41 let f = std::fs::read(fname.clone());
42 if f.is_err() {
43 debug!(
44 "Could not find file {} for var {}, loading from default",
45 fname, varname
46 );
47 default.to_vec()
48 } else {
49 f.unwrap()
50 }
51 }
52 Err(_) => default.to_vec(),
53 }
54}
55
56impl TlsConfig {
57 pub fn new() -> Self {
58 debug!("Configuring TlsConfig with nobody identity");
59 let nobody_crt = load_file_or_default("GL_NOBODY_CRT", NOBODY_CRT);
60 let nobody_key = load_file_or_default("GL_NOBODY_KEY", NOBODY_KEY);
61 let ca_crt = load_file_or_default("GL_CA_CRT", CA_RAW);
62 Self::with(nobody_crt, nobody_key, ca_crt)
65 }
66
67 pub fn with<V: AsRef<[u8]>>(crt: V, key: V, ca_crt: V) -> Self {
68 let x509_cert = x509_certificate_from_pem_or_none(&crt);
69
70 let config = ClientTlsConfig::new()
71 .ca_certificate(Certificate::from_pem(ca_crt.as_ref()))
72 .identity(Identity::from_pem(crt, key.as_ref()));
73
74 TlsConfig {
75 inner: config,
76 private_key: Some(key.as_ref().to_vec()),
77 ca: ca_crt.as_ref().to_vec(),
78 x509_cert,
79 }
80 }
81}
82
83impl TlsConfig {
84 pub fn identity(self, cert_pem: Vec<u8>, key_pem: Vec<u8>) -> Self {
91 let x509_cert = x509_certificate_from_pem_or_none(&cert_pem);
92
93 TlsConfig {
94 inner: self.inner.identity(Identity::from_pem(&cert_pem, &key_pem)),
95 private_key: Some(key_pem),
96 x509_cert,
97 ..self
98 }
99 }
100
101 pub fn identity_from_path<P: AsRef<Path>>(self, path: P) -> Result<Self> {
108 let cert_path = path.as_ref().join("client.crt");
109 let key_path = path.as_ref().join("client-key.pem");
110
111 let cert_pem = std::fs::read(cert_path.clone())
112 .with_context(|| format!("Failed to read '{}'", cert_path.display()))?;
113 let key_pem = std::fs::read(key_path.clone())
114 .with_context(|| format!("Failed to read '{}", key_path.display()))?;
115
116 Ok(self.identity(cert_pem, key_pem))
117 }
118
119 pub fn ca_certificate(self, ca: Vec<u8>) -> Self {
125 TlsConfig {
126 inner: self.inner.ca_certificate(Certificate::from_pem(&ca)),
127 ca,
128 ..self
129 }
130 }
131
132 pub fn client_tls_config(&self) -> ClientTlsConfig {
133 self.inner.clone()
134 }
135}
136
137impl Default for TlsConfig {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143fn x509_certificate_from_pem_or_none(pem: impl AsRef<[u8]>) -> Option<X509Certificate> {
147 X509Certificate::from_pem(pem)
148 .map_err(|e| debug!("Failed to parse x509 certificate: {}", e))
149 .ok()
150}
151
152pub fn generate_self_signed_device_cert(
157 node_id: &str,
158 device: &str,
159 subject_alt_names: Vec<String>,
160) -> rcgen::Certificate {
161 let mut params = cert_params_from_template(subject_alt_names);
163
164 params.is_ca = rcgen::IsCa::ExplicitNoCa;
167 params.distinguished_name.push(
168 rcgen::DnType::CommonName,
169 format!("/users/{}/{}", node_id, device),
170 );
171
172 rcgen::Certificate::from_params(params).unwrap()
173}
174
175fn cert_params_from_template(subject_alt_names: Vec<String>) -> rcgen::CertificateParams {
176 let mut params = rcgen::CertificateParams::new(subject_alt_names);
177 params.key_pair = None;
178 params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
179
180 params
182 .distinguished_name
183 .push(rcgen::DnType::CountryName, "US");
184 params
185 .distinguished_name
186 .push(rcgen::DnType::LocalityName, "SAN FRANCISCO");
187 params
188 .distinguished_name
189 .push(rcgen::DnType::OrganizationName, "Blockstream");
190 params
191 .distinguished_name
192 .push(rcgen::DnType::StateOrProvinceName, "CALIFORNIA");
193 params.distinguished_name.push(
194 rcgen::DnType::OrganizationalUnitName,
195 "CertificateAuthority",
196 );
197
198 return params;
199}
200
201#[cfg(test)]
202pub mod tests {
203 use super::*;
204
205 #[test]
206 fn test_generate_self_signed_device_cert() {
207 let device_cert =
208 generate_self_signed_device_cert("mynodeid", "device", vec!["localhost".into()]);
209 assert!(device_cert
210 .serialize_pem()
211 .unwrap()
212 .starts_with("-----BEGIN CERTIFICATE-----"));
213 assert!(device_cert
214 .serialize_private_key_pem()
215 .starts_with("-----BEGIN PRIVATE KEY-----"));
216 }
217}