use std::path::PathBuf;
use std::sync::Arc;
use log::info;
use rcgen::KeyPair;
use reqwest::blocking::{Client, ClientBuilder};
#[doc(inline)]
pub use error::CertificatesError;
use crate::{VAR_DSH_KAFKA_CONFIG_ENDPOINT, VAR_TASK_ID, utils};
mod error;
mod pki_config_dir;
mod sign_certificates;
#[derive(Debug, Clone)]
pub struct Cert {
dsh_ca_certificate_pem: String,
dsh_client_certificate_pem: String,
key_pair: Arc<KeyPair>,
}
impl Cert {
fn new(
dsh_ca_certificate_pem: String,
dsh_client_certificate_pem: String,
key_pair: KeyPair,
) -> Cert {
Self {
dsh_ca_certificate_pem,
dsh_client_certificate_pem,
key_pair: Arc::new(key_pair),
}
}
pub fn from_bootstrap(
config_host: &str,
tenant_name: &str,
task_id: &str,
) -> Result<Self, CertificatesError> {
sign_certificates::sign_certificates(config_host, tenant_name, task_id, false)
}
pub fn from_env() -> Result<Self, CertificatesError> {
if let Ok(cert) = Self::from_pki_config_dir::<std::path::PathBuf>(None) {
Ok(cert)
} else {
Self::sign_new_certificates(false)
}
}
pub fn sign_new_certificates(add_san: bool) -> Result<Self, CertificatesError> {
if let (Ok(config_host), Ok(task_id), Ok(tenant_name)) = (
utils::get_env_var(VAR_DSH_KAFKA_CONFIG_ENDPOINT).map(utils::ensure_https_prefix),
utils::get_env_var(VAR_TASK_ID),
utils::tenant_name(),
) {
sign_certificates::sign_certificates(&config_host, &tenant_name, &task_id, add_san)
} else {
Err(CertificatesError::MisisngInjectedVariables)
}
}
pub fn from_pki_config_dir<P>(path: Option<P>) -> Result<Self, CertificatesError>
where
P: AsRef<std::path::Path>,
{
pki_config_dir::get_pki_certificates(path)
}
pub fn reqwest_client_config(&self) -> reqwest::ClientBuilder {
let (pem_identity, reqwest_cert) = Self::prepare_reqwest_client(
self.dsh_signed_certificate_pem(),
&self.private_key_pem(),
self.dsh_ca_certificate_pem(),
);
reqwest::Client::builder()
.add_root_certificate(reqwest_cert)
.identity(pem_identity)
.tls_backend_rustls()
}
pub fn reqwest_blocking_client_config(&self) -> ClientBuilder {
let (pem_identity, reqwest_cert) = Self::prepare_reqwest_client(
self.dsh_signed_certificate_pem(),
&self.private_key_pem(),
self.dsh_ca_certificate_pem(),
);
Client::builder()
.add_root_certificate(reqwest_cert)
.identity(pem_identity)
.tls_backend_rustls()
}
pub fn dsh_ca_certificate_pem(&self) -> &str {
&self.dsh_ca_certificate_pem
}
pub fn dsh_signed_certificate_pem(&self) -> &str {
&self.dsh_client_certificate_pem
}
pub fn private_key_pkcs8(&self) -> Vec<u8> {
self.key_pair.serialize_der()
}
pub fn private_key_pem(&self) -> String {
self.key_pair.serialize_pem()
}
pub fn public_key_pem(&self) -> String {
self.key_pair.public_key_pem()
}
pub fn public_key_der(&self) -> Vec<u8> {
use rcgen::PublicKeyData;
self.key_pair.subject_public_key_info()
}
pub fn to_files(&self, dir: &PathBuf) -> Result<(), CertificatesError> {
std::fs::create_dir_all(dir)?;
Self::create_file(dir.join("ca.crt"), self.dsh_ca_certificate_pem())?;
Self::create_file(dir.join("client.pem"), self.dsh_signed_certificate_pem())?;
Self::create_file(dir.join("client.key"), self.private_key_pem())?;
Ok(())
}
fn create_file<C: AsRef<[u8]>>(path: PathBuf, contents: C) -> Result<(), CertificatesError> {
std::fs::write(&path, contents)?;
info!("File created ({})", path.display());
Ok(())
}
fn create_identity(
cert: &[u8],
private_key: &[u8],
) -> Result<reqwest::Identity, reqwest::Error> {
let mut ident = private_key.to_vec();
ident.extend_from_slice(b"\n");
ident.extend_from_slice(cert);
reqwest::Identity::from_pem(&ident)
}
fn prepare_reqwest_client(
kafka_certificate: &str,
private_key: &str,
ca_certificate: &str,
) -> (reqwest::Identity, reqwest::tls::Certificate) {
let pem_identity =
Cert::create_identity(kafka_certificate.as_bytes(), private_key.as_bytes())
.expect("Error creating identity. The Kafka certificate or key is invalid.");
let reqwest_cert = reqwest::tls::Certificate::from_pem(ca_certificate.as_bytes())
.expect("Error parsing CA certificate as PEM. The certificate is invalid.");
(pem_identity, reqwest_cert)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rcgen::{CertifiedKey, generate_simple_self_signed};
use std::sync::OnceLock;
use openssl::pkey::PKey;
static TEST_CERTIFICATES: OnceLock<Cert> = OnceLock::new();
fn set_test_cert() -> Cert {
let subject_alt_names = vec!["hello.world.example".to_string(), "localhost".to_string()];
let CertifiedKey { cert, signing_key } =
generate_simple_self_signed(subject_alt_names).unwrap();
Cert::new(cert.pem(), cert.pem(), signing_key)
}
#[test]
fn test_private_key_pem() {
let cert = TEST_CERTIFICATES.get_or_init(set_test_cert);
let der = cert.key_pair.serialize_der();
let pkey = PKey::private_key_from_der(der.as_slice()).unwrap();
let pkey_pem_bytes = pkey.private_key_to_pem_pkcs8().unwrap();
let key_pem = cert.private_key_pem();
let pkey_pem = String::from_utf8_lossy(&pkey_pem_bytes);
assert_eq!(key_pem, pkey_pem);
}
#[test]
fn test_public_key_pem() {
let cert = TEST_CERTIFICATES.get_or_init(set_test_cert);
let der = cert.key_pair.serialize_der();
let pkey = PKey::private_key_from_der(&der).unwrap();
let pkey_pub_pem_bytes = pkey.public_key_to_pem().unwrap();
let pub_pem = cert.public_key_pem();
let pkey_pub_pem = String::from_utf8_lossy(&pkey_pub_pem_bytes);
assert_eq!(pub_pem, pkey_pub_pem);
}
#[test]
fn test_public_key_der() {
let cert = TEST_CERTIFICATES.get_or_init(set_test_cert);
let der = cert.key_pair.serialize_der();
let pkey = PKey::private_key_from_der(&der).unwrap();
let pkey_pub_der = pkey.public_key_to_der().unwrap();
let pub_der = cert.public_key_der();
assert_eq!(pub_der, pkey_pub_der);
}
#[test]
fn test_private_key_pkcs8() {
let cert = TEST_CERTIFICATES.get_or_init(set_test_cert);
let der = cert.key_pair.serialize_der();
let pkey = PKey::private_key_from_der(&der).unwrap();
let pkey = pkey.private_key_to_pkcs8().unwrap();
let key = cert.private_key_pkcs8();
assert_eq!(key, pkey);
}
#[test]
fn test_dsh_ca_certificate_pem() {
let cert = TEST_CERTIFICATES.get_or_init(set_test_cert);
let pem = cert.dsh_ca_certificate_pem();
assert_eq!(pem, cert.dsh_ca_certificate_pem);
}
#[test]
fn test_dsh_signed_certificate_pem() {
let cert = TEST_CERTIFICATES.get_or_init(set_test_cert);
let pem = cert.dsh_signed_certificate_pem();
assert_eq!(pem, cert.dsh_client_certificate_pem);
}
#[test]
fn test_write_files() {
let cert = TEST_CERTIFICATES.get_or_init(set_test_cert);
let dir = PathBuf::from("test_files");
cert.to_files(&dir).unwrap();
let dir = "test_files";
assert!(std::path::Path::new(&format!("{}/ca.crt", dir)).exists());
assert!(std::path::Path::new(&format!("{}/client.pem", dir)).exists());
assert!(std::path::Path::new(&format!("{}/client.key", dir)).exists());
}
#[test]
fn test_create_identity() {
let cert = TEST_CERTIFICATES.get_or_init(set_test_cert);
let identity = Cert::create_identity(
cert.dsh_signed_certificate_pem().as_bytes(),
cert.private_key_pem().as_bytes(),
);
assert!(identity.is_ok());
}
}