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.
156/// It can take an optional key pair to create the certificate from instead of
157/// generating a key pair from scratch.
158pub fn generate_self_signed_device_cert(
159    node_id: &str,
160    device: &str,
161    subject_alt_names: Vec<String>,
162    key_pair: Option<rcgen::KeyPair>,
163) -> rcgen::Certificate {
164    // Configure the certificate.
165    let mut params = cert_params_from_template(subject_alt_names);
166
167    // Is a leaf certificate only so it is not allowed to sign child
168    // certificates.
169    params.is_ca = rcgen::IsCa::ExplicitNoCa;
170    params.distinguished_name.push(
171        rcgen::DnType::CommonName,
172        format!("/users/{}/{}", node_id, device),
173    );
174
175    // Start from an empty key pair.
176    params.key_pair = key_pair;
177    params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
178
179    rcgen::Certificate::from_params(params).unwrap()
180}
181
182fn cert_params_from_template(subject_alt_names: Vec<String>) -> rcgen::CertificateParams {
183    let mut params = rcgen::CertificateParams::new(subject_alt_names);
184
185    // Certificate can be used to issue unlimited sub certificates for devices.
186    params
187        .distinguished_name
188        .push(rcgen::DnType::CountryName, "US");
189    params
190        .distinguished_name
191        .push(rcgen::DnType::LocalityName, "SAN FRANCISCO");
192    params
193        .distinguished_name
194        .push(rcgen::DnType::OrganizationName, "Blockstream");
195    params
196        .distinguished_name
197        .push(rcgen::DnType::StateOrProvinceName, "CALIFORNIA");
198    params.distinguished_name.push(
199        rcgen::DnType::OrganizationalUnitName,
200        "CertificateAuthority",
201    );
202
203    params
204}
205
206pub fn generate_ecdsa_key_pair() -> rcgen::KeyPair {
207    rcgen::KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap()
208}
209
210#[cfg(test)]
211pub mod tests {
212    use rcgen::KeyPair;
213
214    use super::*;
215
216    #[test]
217    fn test_generate_self_signed_device_cert() {
218        let device_cert =
219            generate_self_signed_device_cert("mynodeid", "device", vec!["localhost".into()], None);
220        assert!(device_cert
221            .serialize_pem()
222            .unwrap()
223            .starts_with("-----BEGIN CERTIFICATE-----"));
224        assert!(device_cert
225            .serialize_private_key_pem()
226            .starts_with("-----BEGIN PRIVATE KEY-----"));
227    }
228
229    #[test]
230    fn test_generate_self_signed_device_cert_from_pem() {
231        let kp = generate_ecdsa_key_pair();
232        let keys = KeyPair::from_der(kp.serialized_der()).unwrap();
233        let cert = generate_self_signed_device_cert(
234            "mynodeid",
235            "device",
236            vec!["localhost".into()],
237            Some(keys),
238        );
239        assert!(kp.serialize_pem() == cert.get_key_pair().serialize_pem());
240    }
241}