#![cfg(unix)]
use std::fmt;
use serde::{Deserialize, Serialize};
pub(crate) const SECRET_PLACEHOLDER_PREFIX: &str = "BUX_SECRET";
#[derive(Clone, Serialize, Deserialize)]
pub struct Secret {
pub name: String,
pub hosts: Vec<String>,
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) placeholder: Option<String>,
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Secret")
.field("name", &self.name)
.field("hosts", &self.hosts)
.field("placeholder", &self.placeholder_str())
.field("value", &"[REDACTED]")
.finish()
}
}
impl Secret {
#[must_use]
pub fn new(
name: impl Into<String>,
hosts: impl IntoIterator<Item = impl Into<String>>,
value: impl Into<String>,
) -> Self {
Self {
name: name.into(),
hosts: hosts.into_iter().map(Into::into).collect(),
value: value.into(),
placeholder: None,
}
}
#[must_use]
pub fn placeholder_str(&self) -> String {
self.placeholder
.clone()
.unwrap_or_else(|| default_placeholder(&self.name))
}
#[must_use]
pub(crate) fn to_shim_secret(&self) -> bux_shim::ShimSecret {
bux_shim::ShimSecret {
name: self.name.clone(),
hosts: self.hosts.clone(),
placeholder: self.placeholder_str(),
value: self.value.clone(),
}
}
}
#[must_use]
pub(crate) fn default_placeholder(name: &str) -> String {
format!("<{SECRET_PLACEHOLDER_PREFIX}:{name}>")
}
#[derive(Clone)]
pub(crate) struct LiveSecrets {
pub(crate) secrets: Vec<Secret>,
pub(crate) ca_cert_pem: String,
pub(crate) ca_key_pem: String,
}
impl fmt::Debug for LiveSecrets {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LiveSecrets")
.field("secrets", &self.secrets)
.field(
"ca_cert_pem",
&format!("<{} bytes>", self.ca_cert_pem.len()),
)
.field("ca_key_pem", &"[REDACTED]")
.finish()
}
}
impl LiveSecrets {
pub(crate) fn mint(secrets: Vec<Secret>) -> crate::Result<Self> {
let (ca_cert_pem, ca_key_pem) = mint_mitm_ca()?;
Ok(Self {
secrets,
ca_cert_pem,
ca_key_pem,
})
}
#[must_use]
pub(crate) fn to_shim_secrets(&self) -> Vec<bux_shim::ShimSecret> {
self.secrets.iter().map(Secret::to_shim_secret).collect()
}
}
fn mint_mitm_ca() -> crate::Result<(String, String)> {
use rcgen::{
BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair,
KeyUsagePurpose,
};
use time::OffsetDateTime;
let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
.map_err(|e| crate::Error::InvalidConfig(format!("MITM CA key generation failed: {e}")))?;
let mut params = CertificateParams::default();
params.distinguished_name = {
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "bux MITM CA");
dn
};
let now = OffsetDateTime::now_utc();
params.not_before = now - time::Duration::minutes(1);
params.not_after = now + time::Duration::days(365 * 10);
params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
params.key_usages = vec![KeyUsagePurpose::CrlSign, KeyUsagePurpose::KeyCertSign];
let cert = params.self_signed(&key_pair).map_err(|e| {
crate::Error::InvalidConfig(format!("MITM CA self-signed cert failed: {e}"))
})?;
Ok((cert.pem(), key_pair.serialize_pem()))
}
#[derive(Debug, Clone, Default)]
pub struct StartOptions {
pub ready_timeout: Option<std::time::Duration>,
pub secrets: Vec<Secret>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "tests")]
mod tests {
use super::*;
#[test]
fn placeholder_default() {
let s = Secret::new("TOKEN", ["api.example.com"], "s3cr3t");
assert_eq!(s.placeholder_str(), "<BUX_SECRET:TOKEN>");
let dbg = format!("{s:?}");
assert!(!dbg.contains("s3cr3t"));
assert!(dbg.contains("REDACTED"));
}
#[test]
fn mint_produces_pem() {
let live = LiveSecrets::mint(vec![Secret::new("A", ["h"], "v")]).unwrap();
assert!(live.ca_cert_pem.contains("BEGIN CERTIFICATE"));
}
fn ca_not_after(pem: &str) -> time::OffsetDateTime {
let (_, block) = x509_parser::pem::parse_x509_pem(pem.as_bytes()).unwrap();
block
.parse_x509()
.unwrap()
.validity()
.not_after
.to_datetime()
}
#[test]
fn mint_ca_not_after_is_ten_years() {
let live = LiveSecrets::mint(vec![Secret::new("A", ["h"], "v")]).unwrap();
let span = ca_not_after(&live.ca_cert_pem) - time::OffsetDateTime::now_utc();
assert!(
span >= time::Duration::days(365 * 10 - 1)
&& span <= time::Duration::days(365 * 10 + 1),
"not_after delta {span:?}"
);
}
}