Files
certforge/src/state.rs
T
Roman KivalinandClaude Opus 4.6 c1fdfba3fe Initial implementation of certforge ACME certificate manager
Rust CLI tool for automated ACME certificate management with:
- DNS-01 challenge via RFC 2136 dynamic DNS updates with TSIG auth
- DANE TLSA record publication (RFC 6698/7671/7672)
- systemd credential integration (systemd-creds encrypt/decrypt)
- systemd unit management via D-Bus (zbus)
- Post-renewal hooks (systemctl reload/restart, arbitrary commands)
- Key rotation with DANE pre-publication protocol
- TOML configuration with per-zone DNS overrides

Tested against Let's Encrypt staging. 12 unit tests, zero warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:07:27 +02:00

61 lines
2.0 KiB
Rust

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
/// Persistent state for tracking pre-publication and pending operations.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct State {
/// Pending key rotations keyed by certificate name.
#[serde(default)]
pub pending_rotations: HashMap<String, PendingRotation>,
}
/// State for a pending key rotation with DANE pre-publication.
#[derive(Debug, Serialize, Deserialize)]
pub struct PendingRotation {
/// Path to the encrypted pending key
pub pending_key_path: PathBuf,
/// When the new TLSA was published
pub published_at: DateTime<Utc>,
/// The old TLSA TTL in seconds (we must wait this long)
pub old_ttl: u32,
}
impl PendingRotation {
/// Check if enough time has elapsed for the old TLSA TTL to expire.
pub fn ttl_expired(&self) -> bool {
let elapsed = Utc::now() - self.published_at;
elapsed.num_seconds() >= self.old_ttl as i64
}
}
impl State {
/// Load state from the state directory.
pub fn load(state_dir: &Path) -> Result<Self> {
let path = state_dir.join("state.json");
if !path.exists() {
return Ok(Self::default());
}
let data = std::fs::read_to_string(&path)
.map_err(|e| Error::State(format!("failed to read state file: {e}")))?;
let state: State = serde_json::from_str(&data)
.map_err(|e| Error::State(format!("failed to parse state file: {e}")))?;
Ok(state)
}
/// Save state to the state directory.
pub fn save(&self, state_dir: &Path) -> Result<()> {
std::fs::create_dir_all(state_dir)?;
let path = state_dir.join("state.json");
let data = serde_json::to_string_pretty(self)
.map_err(|e| Error::State(format!("failed to serialize state: {e}")))?;
std::fs::write(&path, data)?;
Ok(())
}
}