Skip to main content

eneros-trust

Crate Index

eneros-trust

eneros-trust implements the foundation of EnerOS’s Zero Trust security model: built-in CA, mTLS mutual authentication, and automatic certificate rotation. All inter-service communication within the system enforces mTLS by default, with no plaintext transmission. This is the key foundation for EnerOS to meet compliance requirements such as NERC CIP / IEC 62443 / MLPS 2.0 (Multi-Level Protection Scheme).

Responsibilities

eneros-trust handles the following core responsibilities:

  • Internal CA: Issues service certificates, supporting both ECDSA P-256 (recommended) and RSA 4096 key algorithms
  • mTLS Acceptor: Replaces the standard TLS acceptor, enforcing client certificate verification, rejecting connections without certificates
  • Automatic Certificate Rotation: Auto-rotates 30 days before expiry, zero downtime, smooth transition between old and new certificates
  • Certificate Revocation List (CRL): Supports active revocation and OCSP stapling, timely banning of stolen certificates
  • Trust Anchor: Root certificate distribution and update, cross-region trust establishment
  • Certificate Storage: File-system based secure storage, permission 600, ownership isolation
  • Key Archival: Historical key archival and compliance audit

Key Types and Interfaces

CaManager

use rcgen::{Certificate, PrivateKey};
use std::path::Path;

pub struct CaManager {
    ca_cert: Certificate,
    ca_key: PrivateKey,
    validity_days: u32,
    algorithm: KeyAlgorithm,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyAlgorithm {
    EcdsaP256,
    Rsa4096,
}

impl Default for KeyAlgorithm {
    fn default() -> Self {
        Self::EcdsaP256
    }
}

impl CaManager {
    pub fn initialize(common_name: &str) -> Result<Self> {
        let params = rcgen::CertificateParams::new(vec![common_name.to_string()]);
        let mut params = params;
        params.distinguished_name = rcgen::DistinguishedName::new();
        params.distinguished_name.push(
            rcgen::DnType::CommonName,
            common_name,
        );
        params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(5));
        params.key_pair = Some(rcgen::KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256)?);

        let ca_cert = rcgen::Certificate::from_params(params)?;
        let ca_key = ca_cert.serialize_private_key_der();

        Ok(Self {
            ca_cert,
            ca_key: PrivateKey(ca_key),
            validity_days: 3650,
            algorithm: KeyAlgorithm::EcdsaP256,
        })
    }

    pub fn save_to_dir(&self, dir: impl AsRef<Path>) -> Result<()> {
        let dir = dir.as_ref();
        std::fs::create_dir_all(dir)?;
        let cert_path = dir.join("ca.crt");
        let key_path = dir.join("ca.key");

        let cert_pem = self.ca_cert.serialize_pem()?;
        std::fs::write(&cert_path, cert_pem)?;
        set_permissions_600(&cert_path)?;

        std::fs::write(&key_path, &self.ca_key.0)?;
        set_permissions_600(&key_path)?;

        Ok(())
    }

    pub fn load_from_dir(dir: impl AsRef<Path>) -> Result<Self> {
        let dir = dir.as_ref();
        let cert_pem = std::fs::read_to_string(dir.join("ca.crt"))?;
        let key_der = std::fs::read(dir.join("ca.key"))?;
        // Parse and rebuild CA
        Self::parse(&cert_pem, &key_der)
    }

    fn parse(cert_pem: &str, key_der: &[u8]) -> Result<Self> {
        // Actual implementation uses rustls-pemfile / x509-parser for parsing
        unimplemented!()
    }

    pub fn issue(&self, subject: &str) -> Result<(Certificate, PrivateKey)> {
        let mut params = rcgen::CertificateParams::new(vec![subject.to_string()]);
        params.distinguished_name.push(
            rcgen::DnType::CommonName,
            subject,
        );
        params.key_pair = Some(rcgen::KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256)?);

        let cert = rcgen::Certificate::from_params(params)?;
        let signed = cert.serialize_pem_with_signer(&self.ca_cert)?;
        let key = cert.serialize_private_key_der();

        Ok((cert, PrivateKey(key)))
    }

    pub fn revoke(&self, cert: &Certificate) -> Result<()> {
        // Add certificate serial number to CRL
        Ok(())
    }

    pub fn trust_anchor(&self) -> TrustAnchor {
        TrustAnchor {
            cert_pem: self.ca_cert.serialize_pem().unwrap(),
        }
    }
}

fn set_permissions_600(path: &Path) -> Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = std::fs::metadata(path)?.permissions();
        perms.set_mode(0o600);
        std::fs::set_permissions(path, perms)?;
    }
    Ok(())
}

CertManager

use std::time::Duration;
use tokio::time::interval;

pub struct CertManager {
    ca: CaManager,
    rotation_threshold: Duration,
    current_cert: Option<Certificate>,
    current_key: Option<PrivateKey>,
}

impl CertManager {
    pub fn new(ca: CaManager) -> Self {
        Self {
            ca,
            rotation_threshold: Duration::from_secs(30 * 24 * 3600),
            current_cert: None,
            current_key: None,
        }
    }

    pub fn rotation_threshold(mut self, threshold: Duration) -> Self {
        self.rotation_threshold = threshold;
        self
    }

    pub async fn start_rotation_loop(&self) {
        let mut ticker = interval(Duration::from_secs(3600));
        loop {
            ticker.tick().await;
            if let Some(cert) = &self.current_cert {
                if self.should_rotate(cert) {
                    self.rotate().await;
                }
            }
        }
    }

    fn should_rotate(&self, cert: &Certificate) -> bool {
        // Check if certificate is about to expire
        let expires_at = cert.get_expiration_time();
        let remaining = expires_at - chrono::Utc::now();
        remaining < self.rotation_threshold
    }

    async fn rotate(&self) {
        // 1. Issue new certificate
        // 2. Smooth switch (old and new certificates accepted in parallel)
        // 3. Add old certificate to CRL
        // 4. Notify all dependent services
    }

    pub fn current_cert(&self) -> Option<&Certificate> {
        self.current_cert.as_ref()
    }

    pub fn issue_for(&self, subject: &str) -> Result<(Certificate, PrivateKey)> {
        self.ca.issue(subject)
    }
}

MtlsAcceptor

use tokio::net::TcpStream;
use rustls::ServerConfig;

pub struct MtlsAcceptor {
    trust_anchor: TrustAnchor,
    server_config: ServerConfig,
    verify_client: bool,
    min_protocol: TlsVersion,
    max_protocol: TlsVersion,
}

#[derive(Debug, Clone, Copy)]
pub struct TlsVersion {
    pub major: u8,
    pub minor: u8,
}

impl TlsVersion {
    pub const TLS_1_3: Self = Self { major: 3, minor: 4 };
    pub const TLS_1_2: Self = Self { major: 3, minor: 3 };
}

#[derive(Debug, Clone)]
pub struct TrustAnchor {
    pub cert_pem: String,
}

pub struct MtlsAcceptorBuilder {
    trust_anchor: Option<TrustAnchor>,
    server_cert: Option<Certificate>,
    server_key: Option<PrivateKey>,
    verify_client: bool,
    min_protocol: TlsVersion,
    max_protocol: TlsVersion,
}

impl MtlsAcceptorBuilder {
    pub fn new() -> Self {
        Self {
            trust_anchor: None,
            server_cert: None,
            server_key: None,
            verify_client: true,
            min_protocol: TlsVersion::TLS_1_2,
            max_protocol: TlsVersion::TLS_1_3,
        }
    }

    pub fn trust_anchor(mut self, ta: TrustAnchor) -> Self {
        self.trust_anchor = Some(ta);
        self
    }

    pub fn server_cert(mut self, cert: Certificate, key: PrivateKey) -> Self {
        self.server_cert = Some(cert);
        self.server_key = Some(key);
        self
    }

    pub fn verify_client(mut self, verify: bool) -> Self {
        self.verify_client = verify;
        self
    }

    pub fn min_protocol(mut self, v: TlsVersion) -> Self {
        self.min_protocol = v;
        self
    }

    pub fn build(self) -> Result<MtlsAcceptor> {
        let trust_anchor = self.trust_anchor.ok_or_else(|| {
            EnerOSError::Internal("trust_anchor required".into())
        })?;
        let server_cert = self.server_cert.ok_or_else(|| {
            EnerOSError::Internal("server_cert required".into())
        })?;
        let server_key = self.server_key.ok_or_else(|| {
            EnerOSError::Internal("server_key required".into())
        })?;

        let server_config = Self::build_server_config(
            &server_cert,
            &server_key,
            &trust_anchor,
            self.verify_client,
        )?;

        Ok(MtlsAcceptor {
            trust_anchor,
            server_config,
            verify_client: self.verify_client,
            min_protocol: self.min_protocol,
            max_protocol: self.max_protocol,
        })
    }

    fn build_server_config(
        cert: &Certificate,
        key: &PrivateKey,
        anchor: &TrustAnchor,
        verify_client: bool,
    ) -> Result<ServerConfig> {
        let mut roots = rustls::RootCertStore::empty();
        roots.add(anchor.cert_pem.as_bytes())?;

        let client_auth = if verify_client {
            rustls::server::AllowAnyAuthenticatedClient::new(roots).boxed()
        } else {
            rustls::server::AllowAnyAnonymousOrAuthenticatedClient::new(roots).boxed()
        };

        let server_config = ServerConfig::builder()
            .with_safe_defaults()
            .with_client_cert_verifier(client_auth)
            .with_single_cert(
                vec![rustls::Certificate(cert.serialize_der()?)],
                rustls::PrivateKey(key.0.clone()),
            )?;

        Ok(server_config)
    }
}

impl MtlsAcceptor {
    pub fn builder() -> MtlsAcceptorBuilder {
        MtlsAcceptorBuilder::new()
    }

    pub async fn accept(&self, stream: TcpStream) -> Result<TlsStream> {
        let acceptor = tokio_rustls::TlsAcceptor::from(
            std::sync::Arc::new(self.server_config.clone())
        );
        let tls_stream = acceptor.accept(stream).await?;
        Ok(TlsStream { inner: tls_stream })
    }
}

pub struct TlsStream {
    inner: tokio_rustls::server::TlsStream<TcpStream>,
}

CRL / Revocation

pub struct CrlManager {
    revoked: std::collections::HashSet<SerialNumber>,
    last_update: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct SerialNumber(pub String);

impl CrlManager {
    pub fn new() -> Self {
        Self {
            revoked: std::collections::HashSet::new(),
            last_update: chrono::Utc::now(),
        }
    }

    pub fn revoke(&mut self, serial: SerialNumber) {
        self.revoked.insert(serial);
        self.last_update = chrono::Utc::now();
    }

    pub fn is_revoked(&self, serial: &SerialNumber) -> bool {
        self.revoked.contains(serial)
    }

    pub fn to_pem(&self) -> String {
        let mut lines = vec!["-----BEGIN X509 CRL-----".to_string()];
        for serial in &self.revoked {
            lines.push(serial.0.clone());
        }
        lines.push(format!("last_update: {}", self.last_update));
        lines.push("-----END X509 CRL-----".to_string());
        lines.join("\n")
    }
}

Core API

MethodSignatureDescription
CaManager::initializefn initialize(common_name: &str) -> Result<Self>Initialize CA
CaManager::save_to_dirfn save_to_dir(&self, dir: impl AsRef<Path>) -> Result<()>Persist CA
CaManager::load_from_dirfn load_from_dir(dir: impl AsRef<Path>) -> Result<Self>Load CA
CaManager::issuefn issue(&self, subject: &str) -> Result<(Certificate, PrivateKey)>Issue certificate
CaManager::revokefn revoke(&self, cert: &Certificate) -> Result<()>Revoke certificate
CaManager::trust_anchorfn trust_anchor(&self) -> TrustAnchorGet trust anchor
CertManager::newfn new(ca: CaManager) -> SelfConstruct certificate manager
CertManager::rotation_thresholdfn rotation_threshold(self, t: Duration) -> SelfSet rotation threshold
CertManager::start_rotation_loopasync fn start_rotation_loop(&self)Start rotation loop
CertManager::current_certfn current_cert(&self) -> Option<&Certificate>Current certificate
MtlsAcceptor::builderfn builder() -> MtlsAcceptorBuilderConstruct mTLS acceptor
MtlsAcceptor::acceptasync fn accept(&self, stream: TcpStream) -> Result<TlsStream>Accept connection
CrlManager::revokefn revoke(&mut self, serial: SerialNumber)Add to CRL
CrlManager::is_revokedfn is_revoked(&self, serial: &SerialNumber) -> boolCheck revocation status

Usage Examples

Initialize CA

use eneros_trust::{CaManager, CertManager};
use std::time::Duration;

let ca = CaManager::initialize("eneros-ca")?;
ca.save_to_dir("/var/lib/eneros/ca")?;

let cert_mgr = CertManager::new(ca)
    .rotation_threshold(Duration::from_secs(30 * 24 * 3600));

// Start auto-rotation background task
tokio::spawn(async move {
    cert_mgr.start_rotation_loop().await;
});

Enable mTLS on Server

use eneros_trust::MtlsAcceptor;
use axum::Server;

let acceptor = MtlsAcceptor::builder()
    .trust_anchor(ca.trust_anchor())
    .server_cert(server_cert, server_key)
    .verify_client(true)
    .min_protocol(eneros_trust::TlsVersion::TLS_1_2)
    .build()?;

let listener = tokio::net::TcpListener::bind("0.0.0.0:8443").await?;
loop {
    let (stream, _) = listener.accept().await?;
    let acceptor = acceptor.clone();
    tokio::spawn(async move {
        match acceptor.accept(stream).await {
            Ok(tls_stream) => {
                tracing::info!("mTLS connection established");
                // Handle tls_stream
            }
            Err(e) => tracing::warn!("mTLS handshake failed: {}", e),
        }
    });
}

Issue Client Certificate

use eneros_trust::CaManager;

let ca = CaManager::load_from_dir("/var/lib/eneros/ca")?;

let (cert, key) = ca.issue("agent-01.eneros.local")?;
cert.write_pem("agent-01.crt")?;
key.write_pem("agent-01.key")?;

println!("Certificate issued for agent-01, valid for {} days", 90);

Revoke Certificate

use eneros_trust::{CaManager, CrlManager, SerialNumber};

let mut crl = CrlManager::new();
crl.revoke(SerialNumber("agent-05".into()));

let ca = CaManager::load_from_dir("/var/lib/eneros/ca")?;
ca.revoke(&stolen_cert)?;

println!("CRL PEM:");
println!("{}", crl.to_pem());

Client Using mTLS Connection

use eneros_trust::{MtlsConnector, TrustAnchor};

let connector = MtlsConnector::builder()
    .trust_anchor(server_trust_anchor)
    .client_cert(client_cert, client_key)
    .server_name("api.eneros.local")
    .build()?;

let stream = tokio::net::TcpStream::connect("api.eneros.local:8443").await?;
let tls_stream = connector.connect(stream).await?;
println!("mTLS client connection established");

Certificate Lifecycle

Issue (issue)

Distribute — write to /var/lib/eneros/certs/<subject>.{crt,key}

Use (active) — service loads certificate on startup

30 days before expiry (rotation_threshold)

Auto-rotate (rotate) — issue new certificate + smooth switch

Old certificate added to CRL (revoke)

Archive — historical keys archived to WORM storage

Configuration Parameter Table

[trust] Configuration Section

FieldTypeDefaultDescription
ca_dirString”/var/lib/eneros/ca”CA private key and certificate directory
cert_validity_daysu3290Certificate validity period (days)
rotation_threshold_daysu3230Days before expiry to trigger rotation
algorithmKeyAlgorithmEcdsaP256Key algorithm
min_tls_versionString”1.2”Minimum TLS version
verify_clientbooltrueWhether to enforce client certificate verification
crl_enabledbooltrueEnable CRL
ocsp_staplingbooltrueEnable OCSP stapling

KeyAlgorithm Algorithms

AlgorithmKey LengthPerformanceRecommended
EcdsaP256256 bitHigh✓ (default)
Rsa40964096 bitMediumFor legacy system compatibility

TlsVersion Support Matrix

VersionSupportedDescription
TLS 1.0 / 1.1Deprecated, known vulnerabilities
TLS 1.2Default minimum version
TLS 1.3Recommended version, optimal performance and security

Performance Metrics

Test environment: 4 cores / 8GB / Ubuntu 22.04 / Rust 1.78 release build.

OperationLatency
CA initialization< 100ms
Certificate issuance< 50ms
mTLS handshake (ECDSA P-256)< 5ms
mTLS handshake (RSA 4096)< 15ms
CRL check< 1μs
Certificate rotation< 1s (including smooth switch)
OCSP stapling query< 100ms (cache hit < 1μs)

Dependencies

Own Dependencies

[dependencies]
eneros-core = { path = "../core", version = "0.47.0" }
rcgen = "0.13"
rustls = "0.23"
tokio-rustls = "0.26"
rustls-pemfile = "2"
x509-parser = "0.16"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
chrono = { version = "0.4", features = ["serde"] }

Depended On By

eneros-trust is the security foundation of all network services, depended on by the following crates:

  • eneros-gateway — Gateway mTLS authentication
  • eneros-api — API service mTLS acceptor
  • eneros-scada — SCADA channel mTLS
  • eneros-edge — Edge node and central node mTLS
  • eneros-multiregion — Cross-region trust establishment
  • eneros-protocol-mqtt — MQTT broker mTLS
  • eneros-protocol-iccp — ICCP/TASE.2 mTLS

Compliance Mapping

Compliance StandardRequirementeneros-trust Implementation
NERC CIP-005Electronic Security PerimetermTLS mandatory mutual authentication
NERC CIP-007System Security ManagementAutomatic certificate rotation
IEC 62443Security Level SL-3ECDSA P-256 + TLS 1.3
MLPS 2.0 Level 3Identity AuthenticationmTLS + certificate revocation
GDPR / PIPLData Transfer SecurityEnd-to-end encryption

Version and Compatibility

eneros-trust VersionEnerOS VersionKey Changes
0.47.x0.47.xDefault ECDSA P-256 + TLS 1.3
0.46.x0.46.xAdded OCSP stapling
0.45.x (LTS)0.45.xLTS version, security updates until 2028