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();
#[derive(Clone, Debug)]
pub struct TlsConfig {
pub(crate) inner: ClientTlsConfig,
pub(crate) private_key: Option<Vec<u8>>,
pub ca: Vec<u8>,
pub x509_cert: Option<X509Certificate>,
}
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);
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 {
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
}
}
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))
}
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()
}
}
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()
}
pub fn generate_self_signed_device_cert(
node_id: &str,
device: &str,
subject_alt_names: Vec<String>,
key_pair: Option<rcgen::KeyPair>,
) -> rcgen::Certificate {
let mut params = cert_params_from_template(subject_alt_names);
params.is_ca = rcgen::IsCa::ExplicitNoCa;
params.distinguished_name.push(
rcgen::DnType::CommonName,
format!("/users/{}/{}", node_id, device),
);
params.key_pair = key_pair;
params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
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
.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",
);
params
}
pub fn generate_ecdsa_key_pair() -> rcgen::KeyPair {
rcgen::KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap()
}
#[cfg(test)]
pub mod tests {
use rcgen::KeyPair;
use super::*;
#[test]
fn test_generate_self_signed_device_cert() {
let device_cert =
generate_self_signed_device_cert("mynodeid", "device", vec!["localhost".into()], None);
assert!(device_cert
.serialize_pem()
.unwrap()
.starts_with("-----BEGIN CERTIFICATE-----"));
assert!(device_cert
.serialize_private_key_pem()
.starts_with("-----BEGIN PRIVATE KEY-----"));
}
#[test]
fn test_generate_self_signed_device_cert_from_pem() {
let kp = generate_ecdsa_key_pair();
let keys = KeyPair::from_der(kp.serialized_der()).unwrap();
let cert = generate_self_signed_device_cert(
"mynodeid",
"device",
vec!["localhost".into()],
Some(keys),
);
assert!(kp.serialize_pem() == cert.get_key_pair().serialize_pem());
}
}