mirror of
https://github.com/rkivalin/certforge.git
synced 2026-10-13 16:00:44 +00:00
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>
61 lines
2.0 KiB
Rust
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(())
|
|
}
|
|
}
|