use crate::error::WSError;
use crate::provisioning::{CertificateConfig, DeviceIdentity};
use crate::secure_file;
use crate::signature::{KeyPair, PublicKey};
use base64::Engine;
use rcgen::{
BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose,
Ia5String, IsCa, KeyUsagePurpose,
};
use rustls_pki_types::CertificateDer;
use std::fs;
use std::path::Path;
use time::{Duration as TimeDuration, OffsetDateTime};
pub struct PrivateCA {
keypair: KeyPair,
certificate: Vec<u8>,
ca_type: CAType,
config: CAConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CAType {
Root,
Intermediate,
}
#[derive(Debug, Clone)]
pub struct CAConfig {
pub organization: String,
pub common_name: String,
pub country: Option<String>,
pub state: Option<String>,
pub locality: Option<String>,
pub validity_days: u32,
}
impl Default for CAConfig {
fn default() -> Self {
Self {
organization: "Example Organization".to_string(),
common_name: "Example Root CA".to_string(),
country: None,
state: None,
locality: None,
validity_days: 3650, }
}
}
impl CAConfig {
pub fn new(organization: impl Into<String>, common_name: impl Into<String>) -> Self {
Self {
organization: organization.into(),
common_name: common_name.into(),
..Default::default()
}
}
pub fn with_country(mut self, country: impl Into<String>) -> Self {
self.country = Some(country.into());
self
}
pub fn with_state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
pub fn with_locality(mut self, locality: impl Into<String>) -> Self {
self.locality = Some(locality.into());
self
}
pub fn with_validity_days(mut self, days: u32) -> Self {
self.validity_days = days;
self
}
}
impl PrivateCA {
pub fn create_root(config: CAConfig) -> Result<Self, WSError> {
let keypair = KeyPair::generate();
let certificate = Self::create_self_signed_cert(&keypair, &config)?;
Ok(Self {
keypair,
certificate,
ca_type: CAType::Root,
config,
})
}
pub fn create_intermediate(root_ca: &PrivateCA, config: CAConfig) -> Result<Self, WSError> {
if root_ca.ca_type != CAType::Root {
return Err(WSError::InvalidArgument);
}
let keypair = KeyPair::generate();
let certificate = Self::create_signed_cert(&keypair, root_ca, &config)?;
Ok(Self {
keypair,
certificate,
ca_type: CAType::Intermediate,
config,
})
}
pub fn sign_device_certificate(
&self,
device_public_key: &PublicKey,
device_id: &DeviceIdentity,
cert_config: &CertificateConfig,
) -> Result<Vec<u8>, WSError> {
device_id.validate()?;
Self::create_device_cert(device_public_key, self, device_id, cert_config)
}
pub fn sign_device_certificate_with_keypair(
&self,
device_keypair: &KeyPair,
device_id: &DeviceIdentity,
cert_config: &CertificateConfig,
) -> Result<Vec<u8>, WSError> {
device_id.validate()?;
Self::create_device_cert_with_keypair(device_keypair, self, device_id, cert_config)
}
fn create_self_signed_cert(keypair: &KeyPair, config: &CAConfig) -> Result<Vec<u8>, WSError> {
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, &config.common_name);
dn.push(DnType::OrganizationName, &config.organization);
if let Some(country) = &config.country {
dn.push(DnType::CountryName, country);
}
if let Some(state) = &config.state {
dn.push(DnType::StateOrProvinceName, state);
}
if let Some(locality) = &config.locality {
dn.push(DnType::LocalityName, locality);
}
params.distinguished_name = dn;
let now = OffsetDateTime::now_utc();
params.not_before = now;
params.not_after = now + TimeDuration::days(config.validity_days as i64);
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let key_pair_pem = Self::ed25519_to_pem(keypair)?;
let rcgen_keypair = rcgen::KeyPair::from_pem(&key_pair_pem)
.map_err(|e| WSError::HardwareError(format!("Failed to create key pair: {}", e)))?;
let cert = params.self_signed(&rcgen_keypair).map_err(|e| {
WSError::HardwareError(format!("Failed to generate certificate: {}", e))
})?;
let der = cert.der().to_vec();
Ok(der)
}
fn create_signed_cert(
keypair: &KeyPair,
issuer: &PrivateCA,
config: &CAConfig,
) -> Result<Vec<u8>, WSError> {
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, &config.common_name);
dn.push(DnType::OrganizationName, &config.organization);
if let Some(country) = &config.country {
dn.push(DnType::CountryName, country);
}
if let Some(state) = &config.state {
dn.push(DnType::StateOrProvinceName, state);
}
if let Some(locality) = &config.locality {
dn.push(DnType::LocalityName, locality);
}
params.distinguished_name = dn;
let now = OffsetDateTime::now_utc();
params.not_before = now;
params.not_after = now + TimeDuration::days(config.validity_days as i64);
params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
let key_pair_pem = Self::ed25519_to_pem(keypair)?;
let rcgen_keypair = rcgen::KeyPair::from_pem(&key_pair_pem)
.map_err(|e| WSError::HardwareError(format!("Failed to create key pair: {}", e)))?;
let issuer_cert_der = CertificateDer::from(issuer.certificate.clone());
let issuer_cert_params =
CertificateParams::from_ca_cert_der(&issuer_cert_der).map_err(|e| {
WSError::X509Error(format!("Failed to parse issuer certificate: {}", e))
})?;
let issuer_key_pem = Self::ed25519_to_pem(&issuer.keypair)?;
let issuer_keypair = rcgen::KeyPair::from_pem(&issuer_key_pem).map_err(|e| {
WSError::HardwareError(format!("Failed to create issuer key pair: {}", e))
})?;
let issuer_cert = issuer_cert_params
.self_signed(&issuer_keypair)
.map_err(|e| WSError::HardwareError(format!("Failed to create issuer cert: {}", e)))?;
let der = params
.signed_by(&rcgen_keypair, &issuer_cert, &issuer_keypair)
.map_err(|e| WSError::HardwareError(format!("Failed to sign certificate: {}", e)))?
.der()
.to_vec();
Ok(der)
}
fn create_device_cert_with_keypair(
device_keypair: &KeyPair,
ca: &PrivateCA,
device_id: &DeviceIdentity,
config: &CertificateConfig,
) -> Result<Vec<u8>, WSError> {
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, device_id.to_common_name());
dn.push(DnType::OrganizationName, &config.organization);
if let Some(ou) = &config.organizational_unit {
dn.push(DnType::OrganizationalUnitName, ou);
}
params.distinguished_name = dn;
let device_id_str = device_id.id().to_string();
let ia5_string =
Ia5String::try_from(device_id_str.as_str()).map_err(|_| WSError::InvalidArgument)?;
params.subject_alt_names = vec![rcgen::SanType::DnsName(ia5_string)];
let now = OffsetDateTime::now_utc();
params.not_before = now;
params.not_after = now + TimeDuration::days(config.validity_days as i64);
params.is_ca = IsCa::NoCa;
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning];
let key_pair_pem = Self::ed25519_to_pem(device_keypair)?;
let rcgen_keypair = rcgen::KeyPair::from_pem(&key_pair_pem)
.map_err(|e| WSError::HardwareError(format!("Failed to create key pair: {}", e)))?;
let issuer_cert_der = CertificateDer::from(ca.certificate.clone());
let issuer_cert_params = CertificateParams::from_ca_cert_der(&issuer_cert_der)
.map_err(|e| WSError::X509Error(format!("Failed to parse CA certificate: {}", e)))?;
let issuer_key_pem = Self::ed25519_to_pem(&ca.keypair)?;
let issuer_keypair = rcgen::KeyPair::from_pem(&issuer_key_pem)
.map_err(|e| WSError::HardwareError(format!("Failed to create CA key pair: {}", e)))?;
let issuer_cert = issuer_cert_params
.self_signed(&issuer_keypair)
.map_err(|e| WSError::HardwareError(format!("Failed to create CA cert: {}", e)))?;
let der = params
.signed_by(&rcgen_keypair, &issuer_cert, &issuer_keypair)
.map_err(|e| {
WSError::HardwareError(format!("Failed to sign device certificate: {}", e))
})?
.der()
.to_vec();
Ok(der)
}
fn create_device_cert(
device_public_key: &PublicKey,
ca: &PrivateCA,
device_id: &DeviceIdentity,
config: &CertificateConfig,
) -> Result<Vec<u8>, WSError> {
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, device_id.to_common_name());
dn.push(DnType::OrganizationName, &config.organization);
if let Some(ou) = &config.organizational_unit {
dn.push(DnType::OrganizationalUnitName, ou);
}
params.distinguished_name = dn;
let device_id_str = device_id.id().to_string();
let ia5_string =
Ia5String::try_from(device_id_str.as_str()).map_err(|_| WSError::InvalidArgument)?;
params.subject_alt_names = vec![rcgen::SanType::DnsName(ia5_string)];
let now = OffsetDateTime::now_utc();
params.not_before = now;
params.not_after = now + TimeDuration::days(config.validity_days as i64);
params.is_ca = IsCa::NoCa;
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning];
let temp_full_keypair = KeyPair::generate();
let temp_keypair = KeyPair {
sk: temp_full_keypair.sk,
pk: device_public_key.clone(),
};
let key_pair_pem = Self::ed25519_to_pem(&temp_keypair)?;
let rcgen_keypair = rcgen::KeyPair::from_pem(&key_pair_pem)
.map_err(|e| WSError::HardwareError(format!("Failed to create key pair: {}", e)))?;
let issuer_cert_der = CertificateDer::from(ca.certificate.clone());
let issuer_cert_params = CertificateParams::from_ca_cert_der(&issuer_cert_der)
.map_err(|e| WSError::X509Error(format!("Failed to parse CA certificate: {}", e)))?;
let issuer_key_pem = Self::ed25519_to_pem(&ca.keypair)?;
let issuer_keypair = rcgen::KeyPair::from_pem(&issuer_key_pem)
.map_err(|e| WSError::HardwareError(format!("Failed to create CA key pair: {}", e)))?;
let issuer_cert = issuer_cert_params
.self_signed(&issuer_keypair)
.map_err(|e| WSError::HardwareError(format!("Failed to create CA cert: {}", e)))?;
let der = params
.signed_by(&rcgen_keypair, &issuer_cert, &issuer_keypair)
.map_err(|e| {
WSError::HardwareError(format!("Failed to sign device certificate: {}", e))
})?
.der()
.to_vec();
Ok(der)
}
fn ed25519_to_pem(keypair: &KeyPair) -> Result<String, WSError> {
let pem = keypair.sk.to_pem();
Ok((*pem).clone())
}
pub fn certificate(&self) -> &[u8] {
&self.certificate
}
pub fn certificate_pem(&self) -> String {
use pem::Pem;
let pem = Pem::new("CERTIFICATE", self.certificate.clone());
pem::encode(&pem)
}
pub fn ca_type(&self) -> CAType {
self.ca_type
}
pub fn config(&self) -> &CAConfig {
&self.config
}
pub fn save_to_directory(&self, dir: impl AsRef<Path>) -> Result<(), WSError> {
let dir = dir.as_ref();
fs::create_dir_all(dir)
.map_err(|e| WSError::HardwareError(format!("Failed to create directory: {}", e)))?;
let key_path = dir.join("ca.key");
let key_pem = format!(
"-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----\n",
base64::prelude::BASE64_STANDARD.encode(self.keypair.sk.to_bytes())
);
secure_file::write_secure_string(&key_path, &key_pem)
.map_err(|e| WSError::HardwareError(format!("Failed to write key securely: {}", e)))?;
let cert_path = dir.join("ca.crt");
fs::write(cert_path, self.certificate_pem())
.map_err(|e| WSError::HardwareError(format!("Failed to write certificate: {}", e)))?;
Ok(())
}
pub fn load_from_directory(_dir: impl AsRef<Path>) -> Result<Self, WSError> {
Err(WSError::UnsupportedAlgorithm(
"CA loading not yet implemented (placeholder)".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ca_config_builder() {
let config = CAConfig::new("Acme Corp", "Acme Root CA")
.with_country("US")
.with_state("California")
.with_locality("San Francisco")
.with_validity_days(3650);
assert_eq!(config.organization, "Acme Corp");
assert_eq!(config.common_name, "Acme Root CA");
assert_eq!(config.country, Some("US".to_string()));
assert_eq!(config.state, Some("California".to_string()));
assert_eq!(config.locality, Some("San Francisco".to_string()));
assert_eq!(config.validity_days, 3650);
}
#[test]
fn test_create_root_ca() {
let config = CAConfig::new("Test Corp", "Test Root CA");
let ca = PrivateCA::create_root(config).unwrap();
assert_eq!(ca.ca_type(), CAType::Root);
assert_eq!(ca.config().organization, "Test Corp");
assert_eq!(ca.config().common_name, "Test Root CA");
assert!(!ca.certificate().is_empty());
}
#[test]
fn test_create_intermediate_ca() {
let root_config = CAConfig::new("Test Corp", "Test Root CA");
let root_ca = PrivateCA::create_root(root_config).unwrap();
let intermediate_config =
CAConfig::new("Test Corp", "Test Intermediate CA").with_validity_days(1825);
let intermediate_ca =
PrivateCA::create_intermediate(&root_ca, intermediate_config).unwrap();
assert_eq!(intermediate_ca.ca_type(), CAType::Intermediate);
assert_eq!(intermediate_ca.config().common_name, "Test Intermediate CA");
}
#[test]
fn test_cannot_create_intermediate_from_intermediate() {
let root_config = CAConfig::new("Test Corp", "Test Root CA");
let root_ca = PrivateCA::create_root(root_config).unwrap();
let intermediate_config = CAConfig::new("Test Corp", "Test Intermediate CA");
let intermediate_ca =
PrivateCA::create_intermediate(&root_ca, intermediate_config).unwrap();
let config2 = CAConfig::new("Test Corp", "Test Second Intermediate CA");
let result = PrivateCA::create_intermediate(&intermediate_ca, config2);
assert!(result.is_err());
}
#[test]
fn test_sign_device_certificate() {
let root_config = CAConfig::new("Test Corp", "Test Root CA");
let ca = PrivateCA::create_root(root_config).unwrap();
let device_keypair = KeyPair::generate();
let device_id = DeviceIdentity::new("device-123");
let cert_config = CertificateConfig::new("device-123");
let device_cert = ca.sign_device_certificate(&device_keypair.pk, &device_id, &cert_config);
assert!(device_cert.is_ok());
assert!(!device_cert.unwrap().is_empty());
}
#[test]
fn test_ca_certificate_pem() {
let config = CAConfig::new("Test Corp", "Test Root CA");
let ca = PrivateCA::create_root(config).unwrap();
let pem = ca.certificate_pem();
assert!(pem.contains("-----BEGIN CERTIFICATE-----"));
assert!(pem.contains("-----END CERTIFICATE-----"));
}
#[test]
fn test_root_ca_x509_structure() {
use x509_parser::prelude::*;
let config = CAConfig::new("Test Corp", "Test Root CA")
.with_country("US")
.with_state("California");
let ca = PrivateCA::create_root(config).unwrap();
let cert_der = ca.certificate();
let (_, cert) = X509Certificate::from_der(cert_der).unwrap();
let subject = cert.subject();
let cn = subject.iter_common_name().next().unwrap().as_str().unwrap();
assert_eq!(cn, "Test Root CA");
let org = subject
.iter_organization()
.next()
.unwrap()
.as_str()
.unwrap();
assert_eq!(org, "Test Corp");
let basic_constraints = cert.basic_constraints().unwrap();
if let Some(bc) = basic_constraints {
assert!(bc.value.ca, "Root CA certificate should have CA=true");
}
let key_usage = cert.key_usage();
assert!(key_usage.is_ok());
println!("✓ Root CA certificate is valid X.509");
println!(" Subject: {}", cert.subject());
println!(" Issuer: {}", cert.issuer());
println!(" Serial: {}", cert.serial.to_str_radix(16));
println!(" Valid from: {}", cert.validity().not_before);
println!(" Valid to: {}", cert.validity().not_after);
}
#[test]
fn test_device_certificate_x509_structure() {
use x509_parser::prelude::*;
let root_config = CAConfig::new("Test Corp", "Test Root CA");
let ca = PrivateCA::create_root(root_config).unwrap();
let device_keypair = KeyPair::generate();
let device_id = DeviceIdentity::new("device-123");
let cert_config = CertificateConfig::new("device-123")
.with_organization("Test Corp")
.with_organizational_unit("IoT Devices");
let device_cert_der = ca
.sign_device_certificate(&device_keypair.pk, &device_id, &cert_config)
.unwrap();
let (_, cert) = X509Certificate::from_der(&device_cert_der).unwrap();
let subject = cert.subject();
let cn = subject.iter_common_name().next().unwrap().as_str().unwrap();
assert_eq!(cn, "Device device-123");
let org = subject
.iter_organization()
.next()
.unwrap()
.as_str()
.unwrap();
assert_eq!(org, "Test Corp");
let basic_constraints = cert.basic_constraints().unwrap();
if let Some(bc) = basic_constraints {
assert!(!bc.value.ca, "Device certificate should not be a CA");
}
let san = cert.subject_alternative_name();
assert!(san.is_ok(), "Device certificate should have SAN");
let san_value = san.unwrap();
assert!(
san_value.is_some(),
"Device certificate should have SAN value"
);
println!("✓ Device certificate is valid X.509");
println!(" Subject: {}", cert.subject());
println!(" Issuer: {}", cert.issuer());
println!(" Serial: {}", cert.serial.to_str_radix(16));
}
#[test]
fn test_certificate_chain_validation() {
use x509_parser::prelude::*;
let root_config = CAConfig::new("Test Corp", "Test Root CA");
let root_ca = PrivateCA::create_root(root_config).unwrap();
let intermediate_config = CAConfig::new("Test Corp", "Test Intermediate CA");
let intermediate_ca =
PrivateCA::create_intermediate(&root_ca, intermediate_config).unwrap();
let device_keypair = KeyPair::generate();
let device_id = DeviceIdentity::new("device-xyz");
let cert_config = CertificateConfig::new("device-xyz");
let device_cert_der = intermediate_ca
.sign_device_certificate(&device_keypair.pk, &device_id, &cert_config)
.unwrap();
let (_, root_cert) = X509Certificate::from_der(root_ca.certificate()).unwrap();
let (_, intermediate_cert) =
X509Certificate::from_der(intermediate_ca.certificate()).unwrap();
let (_, device_cert) = X509Certificate::from_der(&device_cert_der).unwrap();
assert_eq!(device_cert.issuer(), intermediate_cert.subject());
assert_eq!(intermediate_cert.issuer(), root_cert.subject());
assert_eq!(root_cert.issuer(), root_cert.subject());
println!("✓ Certificate chain is valid");
println!(" Root: {}", root_cert.subject());
println!(" Intermediate: {}", intermediate_cert.subject());
println!(" Device: {}", device_cert.subject());
}
#[cfg(unix)]
#[test]
fn test_ca_save_to_directory_sets_secure_permissions() {
use std::os::unix::fs::PermissionsExt;
let config = CAConfig::new("Test Corp", "Test Root CA");
let ca = PrivateCA::create_root(config).unwrap();
let temp_dir = std::env::temp_dir().join("wsc_test_ca_perms");
ca.save_to_directory(&temp_dir).unwrap();
let key_path = temp_dir.join("ca.key");
let metadata = std::fs::metadata(&key_path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(
mode, 0o600,
"CA private key should have mode 0600, got {:o}",
mode
);
let cert_path = temp_dir.join("ca.crt");
assert!(cert_path.exists(), "Certificate file should exist");
std::fs::remove_dir_all(temp_dir).ok();
}
#[cfg(unix)]
#[test]
fn test_ca_private_key_not_world_readable() {
use std::os::unix::fs::PermissionsExt;
let config = CAConfig::new("Test Corp", "Test Root CA");
let ca = PrivateCA::create_root(config).unwrap();
let temp_dir = std::env::temp_dir().join("wsc_test_ca_no_world");
ca.save_to_directory(&temp_dir).unwrap();
let key_path = temp_dir.join("ca.key");
let metadata = std::fs::metadata(&key_path).unwrap();
let mode = metadata.permissions().mode();
assert_eq!(
mode & 0o077,
0,
"CA private key should not be accessible to group or others, mode: {:o}",
mode & 0o777
);
std::fs::remove_dir_all(temp_dir).ok();
}
}