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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
use anyhow::{Context, Result};
use log::debug;
use std::path::Path;
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
use x509_certificate::X509Certificate;

const CA_RAW: &[u8] = include_str!("../.resources/tls/ca.pem").as_bytes();
const NOBODY_CRT: &[u8] = include_str!(env!("GL_NOBODY_CRT")).as_bytes();
const NOBODY_KEY: &[u8] = include_str!(env!("GL_NOBODY_KEY")).as_bytes();

/// In order to allow the clients to talk to the
/// [`crate::scheduler::Scheduler`] a default certificate and private
/// key is included in this crate. The only service endpoints that can
/// be contacted with this `NOBODY` identity are
/// [`Scheduler.register`] and [`Scheduler.recover`], as these are the
/// endpoints that are used to prove ownership of a node, and
/// returning valid certificates if that proof succeeds.
#[derive(Clone, Debug)]
pub struct TlsConfig {
    pub(crate) inner: ClientTlsConfig,

    /// Copy of the private key in the TLS identity. Stored here in
    /// order to be able to use it in the `AuthLayer`.
    pub(crate) private_key: Option<Vec<u8>>,

    pub ca: Vec<u8>,

    /// The device_crt parsed as an x509 certificate. Used to
    /// validate the common subject name against the node_id
    /// configured on the scheduler.
    pub x509_cert: Option<X509Certificate>,
}

/// Tries to load nobody credentials from a file that is passed by an envvar and
/// defaults to the nobody cert and key paths that have been set during build-
/// time.
fn load_file_or_default(varname: &str, default: &[u8]) -> Vec<u8> {
    match std::env::var(varname) {
        Ok(fname) => {
            debug!("Loading file {} for envvar {}", fname, varname);
            let f = std::fs::read(fname.clone());
            if f.is_err() {
                debug!(
                    "Could not find file {} for var {}, loading from default",
                    fname, varname
                );
                default.to_vec()
            } else {
                f.unwrap()
            }
        }
        Err(_) => default.to_vec(),
    }
}

impl TlsConfig {
    pub fn new() -> Self {
        debug!("Configuring TlsConfig with nobody identity");
        let nobody_crt = load_file_or_default("GL_NOBODY_CRT", NOBODY_CRT);
        let nobody_key = load_file_or_default("GL_NOBODY_KEY", NOBODY_KEY);
        let ca_crt = load_file_or_default("GL_CA_CRT", CA_RAW);
        // it is ok to panic here in case of a broken nobody certificate.
        // We can not do anything at all and should fail loudly!
        Self::with(nobody_crt, nobody_key, ca_crt)
    }

    pub fn with<V: AsRef<[u8]>>(crt: V, key: V, ca_crt: V) -> Self {
        let x509_cert = x509_certificate_from_pem_or_none(&crt);

        let config = ClientTlsConfig::new()
            .ca_certificate(Certificate::from_pem(ca_crt.as_ref()))
            .identity(Identity::from_pem(crt, key.as_ref()));

        TlsConfig {
            inner: config,
            private_key: Some(key.as_ref().to_vec()),
            ca: ca_crt.as_ref().to_vec(),
            x509_cert,
        }
    }
}

impl TlsConfig {
    /// This function is used to upgrade the anonymous `NOBODY`
    /// configuration to a fully authenticated configuration.
    ///
    /// Only non-`NOBODY` configurations are able to talk to their
    /// nodes. If the `TlsConfig` is not upgraded, nodes will reply
    /// with handshake failures, and abort the connection attempt.
    pub fn identity(self, cert_pem: Vec<u8>, key_pem: Vec<u8>) -> Self {
        let x509_cert = x509_certificate_from_pem_or_none(&cert_pem);

        TlsConfig {
            inner: self.inner.identity(Identity::from_pem(&cert_pem, &key_pem)),
            private_key: Some(key_pem),
            x509_cert,
            ..self
        }
    }

    /// Upgrades the connection using an identity based on a certificate
    /// and key from a path.
    ///
    /// The path is a directory that contains a `client.crt` and
    /// a `client-key.pem`-file which contain respectively the certificate
    /// and private key.
    pub fn identity_from_path<P: AsRef<Path>>(self, path: P) -> Result<Self> {
        let cert_path = path.as_ref().join("client.crt");
        let key_path = path.as_ref().join("client-key.pem");

        let cert_pem = std::fs::read(cert_path.clone())
            .with_context(|| format!("Failed to read '{}'", cert_path.display()))?;
        let key_pem = std::fs::read(key_path.clone())
            .with_context(|| format!("Failed to read '{}", key_path.display()))?;

        Ok(self.identity(cert_pem, key_pem))
    }

    /// This function is mostly used to allow running integration
    /// tests against a local mock of the service. It should not be
    /// used in production, since the preconfigured CA ensures that
    /// only the greenlight production servers can complete a valid
    /// handshake.
    pub fn ca_certificate(self, ca: Vec<u8>) -> Self {
        TlsConfig {
            inner: self.inner.ca_certificate(Certificate::from_pem(&ca)),
            ca,
            ..self
        }
    }

    pub fn client_tls_config(&self) -> ClientTlsConfig {
        self.inner.clone()
    }
}

impl Default for TlsConfig {
    fn default() -> Self {
        Self::new()
    }
}

/// A wrapper that returns an Option that contains a `X509Certificate`
/// if it could be parsed from the given `pem` data or None if it could
/// not be parsed. Logs a failed attempt.
fn x509_certificate_from_pem_or_none(pem: impl AsRef<[u8]>) -> Option<X509Certificate> {
    X509Certificate::from_pem(pem)
        .map_err(|e| debug!("Failed to parse x509 certificate: {}", e))
        .ok()
}

/// Generate a new device certificate from a fresh set of keys. The path in the
/// common name (CN) field is "/users/{node_id}/{device}". This certificate is
/// self signed and needs to be signed off by the users certificate authority to
/// be valid. This certificate can not act as a ca and sign sub certificates.
pub fn generate_self_signed_device_cert(
    node_id: &str,
    device: &str,
    subject_alt_names: Vec<String>,
) -> rcgen::Certificate {
    // Configure the certificate.
    let mut params = cert_params_from_template(subject_alt_names);

    // Is a leaf certificate only so it is not allowed to sign child
    // certificates.
    params.is_ca = rcgen::IsCa::ExplicitNoCa;
    params.distinguished_name.push(
        rcgen::DnType::CommonName,
        format!("/users/{}/{}", node_id, device),
    );

    rcgen::Certificate::from_params(params).unwrap()
}

fn cert_params_from_template(subject_alt_names: Vec<String>) -> rcgen::CertificateParams {
    let mut params = rcgen::CertificateParams::new(subject_alt_names);
    params.key_pair = None;
    params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;

    // Certificate can be used to issue unlimited sub certificates for devices.
    params
        .distinguished_name
        .push(rcgen::DnType::CountryName, "US");
    params
        .distinguished_name
        .push(rcgen::DnType::LocalityName, "SAN FRANCISCO");
    params
        .distinguished_name
        .push(rcgen::DnType::OrganizationName, "Blockstream");
    params
        .distinguished_name
        .push(rcgen::DnType::StateOrProvinceName, "CALIFORNIA");
    params.distinguished_name.push(
        rcgen::DnType::OrganizationalUnitName,
        "CertificateAuthority",
    );

    return params;
}

#[cfg(test)]
pub mod tests {
    use super::*;

    #[test]
    fn test_generate_self_signed_device_cert() {
        let device_cert =
            generate_self_signed_device_cert("mynodeid", "device", vec!["localhost".into()]);
        assert!(device_cert
            .serialize_pem()
            .unwrap()
            .starts_with("-----BEGIN CERTIFICATE-----"));
        assert!(device_cert
            .serialize_private_key_pem()
            .starts_with("-----BEGIN PRIVATE KEY-----"));
    }
}