bs_gl_client/
tls.rs

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/// In order to allow the clients to talk to the
12/// [`crate::scheduler::Scheduler`] a default certificate and private
13/// key is included in this crate. The only service endpoints that can
14/// be contacted with this `NOBODY` identity are
15/// [`Scheduler.register`] and [`Scheduler.recover`], as these are the
16/// endpoints that are used to prove ownership of a node, and
17/// returning valid certificates if that proof succeeds.
18#[derive(Clone, Debug)]
19pub struct TlsConfig {
20    pub(crate) inner: ClientTlsConfig,
21
22    /// Copy of the private key in the TLS identity. Stored here in
23    /// order to be able to use it in the `AuthLayer`.
24    pub(crate) private_key: Option<Vec<u8>>,
25
26    pub ca: Vec<u8>,
27
28    /// The device_crt parsed as an x509 certificate. Used to
29    /// validate the common subject name against the node_id
30    /// configured on the scheduler.
31    pub x509_cert: Option<X509Certificate>,
32}
33
34/// Tries to load nobody credentials from a file that is passed by an envvar and
35/// defaults to the nobody cert and key paths that have been set during build-
36/// time.
37fn 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        // it is ok to panic here in case of a broken nobody certificate.
63        // We can not do anything at all and should fail loudly!
64        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    /// This function is used to upgrade the anonymous `NOBODY`
85    /// configuration to a fully authenticated configuration.
86    ///
87    /// Only non-`NOBODY` configurations are able to talk to their
88    /// nodes. If the `TlsConfig` is not upgraded, nodes will reply
89    /// with handshake failures, and abort the connection attempt.
90    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    /// Upgrades the connection using an identity based on a certificate
102    /// and key from a path.
103    ///
104    /// The path is a directory that contains a `client.crt` and
105    /// a `client-key.pem`-file which contain respectively the certificate
106    /// and private key.
107    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    /// This function is mostly used to allow running integration
120    /// tests against a local mock of the service. It should not be
121    /// used in production, since the preconfigured CA ensures that
122    /// only the greenlight production servers can complete a valid
123    /// handshake.
124    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
143/// A wrapper that returns an Option that contains a `X509Certificate`
144/// if it could be parsed from the given `pem` data or None if it could
145/// not be parsed. Logs a failed attempt.
146fn 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
152/// Generate a new device certificate from a fresh set of keys. The path in the
153/// common name (CN) field is "/users/{node_id}/{device}". This certificate is
154/// self signed and needs to be signed off by the users certificate authority to
155/// be valid. This certificate can not act as a ca and sign sub certificates.
156pub fn generate_self_signed_device_cert(
157    node_id: &str,
158    device: &str,
159    subject_alt_names: Vec<String>,
160) -> rcgen::Certificate {
161    // Configure the certificate.
162    let mut params = cert_params_from_template(subject_alt_names);
163
164    // Is a leaf certificate only so it is not allowed to sign child
165    // certificates.
166    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    // Certificate can be used to issue unlimited sub certificates for devices.
181    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}