mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Add identity provisioning with persistent key file
Implement identity management for the FIPS daemon with two modes: Ephemeral (default): A fresh keypair is generated on every start. Key files (fips.key, fips.pub) are written for operator visibility but overwritten on each restart. This is privacy-friendly and requires no configuration. Persistent (opt-in via `node.identity.persistent: true`): Uses three-tier identity resolution: 1. Explicit nsec in config file (advanced users) 2. Persistent key file alongside config (reused across restarts) 3. Generate new keypair, persist to key file Key files follow the SSH id_ed25519/id_ed25519.pub convention: - fips.key: bare bech32 nsec string, mode 0600 - fips.pub: bare bech32 npub string, mode 0644 The daemon always writes fips.pub on startup so operators can find their current identity via `cat /etc/fips/fips.pub`. Identity resolution extracted into testable `resolve_identity()` function in config module. Graceful degradation: key file write failure silently falls back to ephemeral identity. Add `fipsctl keygen` subcommand for manual key generation: - `-d <dir>` output directory (default: /etc/fips) - `-f` force overwrite existing files - `-s` print nsec/npub to stdout instead of writing files - Warns that `persistent: true` must be set in config - Works without a running daemon Includes unit tests for key file read/write roundtrip, file permissions, whitespace trimming, empty file error, path derivation, ephemeral-by-default behavior, ephemeral key cycling, persistent key file loading, and persistent generate-and-reuse lifecycle.
This commit is contained in:
+17
-6
@@ -3,6 +3,7 @@
|
||||
//! Loads configuration and creates the top-level node instance.
|
||||
|
||||
use clap::Parser;
|
||||
use fips::config::{resolve_identity, IdentitySource};
|
||||
use fips::{Config, Node};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{error, info, warn, Level};
|
||||
@@ -63,14 +64,24 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Log identity status
|
||||
if config.has_identity() {
|
||||
info!("Using configured identity");
|
||||
} else {
|
||||
warn!("No identity configured, generating ephemeral keypair");
|
||||
// Identity provisioning: config nsec > key file > generate ephemeral
|
||||
let resolved = match resolve_identity(&config, &loaded_paths) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Failed to resolve identity: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
match &resolved.source {
|
||||
IdentitySource::Config => info!("Using identity from configuration"),
|
||||
IdentitySource::KeyFile(p) => info!(path = %p.display(), "Loaded persistent identity from key file"),
|
||||
IdentitySource::Generated(p) => info!(path = %p.display(), "Generated persistent identity, saved to key file"),
|
||||
IdentitySource::Ephemeral => info!("Using ephemeral identity (new keypair each start)"),
|
||||
}
|
||||
|
||||
// Create node
|
||||
// Create node with resolved identity
|
||||
let mut config = config;
|
||||
config.node.identity.nsec = Some(resolved.nsec);
|
||||
info!("Creating node");
|
||||
let mut node = match Node::new(config) {
|
||||
Ok(node) => node,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
//! query command, and pretty-prints the JSON response.
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use fips::config::{write_key_file, write_pub_file};
|
||||
use fips::{encode_nsec, Identity};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -28,6 +30,18 @@ enum Commands {
|
||||
#[command(subcommand)]
|
||||
what: ShowCommands,
|
||||
},
|
||||
/// Generate a new FIPS identity keypair
|
||||
Keygen {
|
||||
/// Output directory for fips.key and fips.pub
|
||||
#[arg(short = 'd', long = "dir", default_value = "/etc/fips")]
|
||||
dir: PathBuf,
|
||||
/// Overwrite existing key files
|
||||
#[arg(short = 'f', long = "force")]
|
||||
force: bool,
|
||||
/// Print nsec and npub to stdout instead of writing files
|
||||
#[arg(short = 's', long = "stdout")]
|
||||
stdout: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
@@ -88,9 +102,61 @@ fn default_socket_path() -> PathBuf {
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Commands that don't require a running daemon
|
||||
match &cli.command {
|
||||
Commands::Keygen {
|
||||
dir,
|
||||
force,
|
||||
stdout,
|
||||
} => {
|
||||
let identity = Identity::generate();
|
||||
let nsec = encode_nsec(&identity.keypair().secret_key());
|
||||
let npub = identity.npub();
|
||||
|
||||
if *stdout {
|
||||
println!("{}", nsec);
|
||||
println!("{}", npub);
|
||||
return;
|
||||
}
|
||||
|
||||
let key_path = dir.join("fips.key");
|
||||
let pub_path = dir.join("fips.pub");
|
||||
|
||||
if key_path.exists() && !force {
|
||||
eprintln!("error: key file already exists: {}", key_path.display());
|
||||
eprintln!("Use --force to overwrite.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if let Err(e) = std::fs::create_dir_all(dir) {
|
||||
eprintln!("error: cannot create directory {}: {}", dir.display(), e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if let Err(e) = write_key_file(&key_path, &nsec) {
|
||||
eprintln!("error: failed to write key file: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if let Err(e) = write_pub_file(&pub_path, &npub) {
|
||||
eprintln!("error: failed to write pub file: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
eprintln!("{}", npub);
|
||||
eprintln!("Key files written to: {}/", dir.display());
|
||||
eprintln!();
|
||||
eprintln!("NOTE: Set 'node.identity.persistent: true' in fips.yaml");
|
||||
eprintln!(" or these keys will be overwritten on next daemon start.");
|
||||
return;
|
||||
}
|
||||
Commands::Show { .. } => {}
|
||||
}
|
||||
|
||||
let socket_path = cli.socket.unwrap_or_else(default_socket_path);
|
||||
let command_name = match &cli.command {
|
||||
Commands::Show { what } => what.command_name(),
|
||||
Commands::Keygen { .. } => unreachable!(),
|
||||
};
|
||||
|
||||
// Connect to the control socket
|
||||
|
||||
@@ -38,6 +38,218 @@ pub use transport::{EthernetConfig, TcpConfig, TransportInstances, TransportsCon
|
||||
/// Default config filename.
|
||||
const CONFIG_FILENAME: &str = "fips.yaml";
|
||||
|
||||
/// Default key filename, placed alongside the config file.
|
||||
const KEY_FILENAME: &str = "fips.key";
|
||||
|
||||
/// Default public key filename, placed alongside the key file.
|
||||
const PUB_FILENAME: &str = "fips.pub";
|
||||
|
||||
/// Derive the key file path from a config file path.
|
||||
pub fn key_file_path(config_path: &Path) -> PathBuf {
|
||||
config_path
|
||||
.parent()
|
||||
.unwrap_or(Path::new("."))
|
||||
.join(KEY_FILENAME)
|
||||
}
|
||||
|
||||
/// Derive the public key file path from a config file path.
|
||||
pub fn pub_file_path(config_path: &Path) -> PathBuf {
|
||||
config_path
|
||||
.parent()
|
||||
.unwrap_or(Path::new("."))
|
||||
.join(PUB_FILENAME)
|
||||
}
|
||||
|
||||
/// Read a bare bech32 nsec from a key file.
|
||||
pub fn read_key_file(path: &Path) -> Result<String, ConfigError> {
|
||||
let contents = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
let nsec = contents.trim().to_string();
|
||||
if nsec.is_empty() {
|
||||
return Err(ConfigError::EmptyKeyFile {
|
||||
path: path.to_path_buf(),
|
||||
});
|
||||
}
|
||||
Ok(nsec)
|
||||
}
|
||||
|
||||
/// Write a bare bech32 nsec to a key file with restricted permissions (mode 0600).
|
||||
pub fn write_key_file(path: &Path, nsec: &str) -> Result<(), ConfigError> {
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path)
|
||||
.map_err(|e| ConfigError::WriteKeyFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
file.write_all(nsec.as_bytes())
|
||||
.map_err(|e| ConfigError::WriteKeyFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
file.write_all(b"\n")
|
||||
.map_err(|e| ConfigError::WriteKeyFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a bare bech32 npub to a public key file (mode 0644).
|
||||
pub fn write_pub_file(path: &Path, npub: &str) -> Result<(), ConfigError> {
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o644)
|
||||
.open(path)
|
||||
.map_err(|e| ConfigError::WriteKeyFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
file.write_all(npub.as_bytes())
|
||||
.map_err(|e| ConfigError::WriteKeyFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
file.write_all(b"\n")
|
||||
.map_err(|e| ConfigError::WriteKeyFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve identity from config and key file.
|
||||
///
|
||||
/// Behavior depends on `node.identity.persistent`:
|
||||
///
|
||||
/// - **`persistent: false`** (default): generate a fresh ephemeral keypair
|
||||
/// every start. Key files are written for operator visibility but overwritten
|
||||
/// on each restart.
|
||||
///
|
||||
/// - **`persistent: true`**: use three-tier resolution:
|
||||
/// 1. Explicit nsec in config — highest priority
|
||||
/// 2. Persistent key file (`fips.key`) — reused across restarts
|
||||
/// 3. Generate new — creates keypair, writes `fips.key` and `fips.pub`
|
||||
///
|
||||
/// - **`nsec` set explicitly**: always uses that, regardless of `persistent`.
|
||||
///
|
||||
/// Returns the nsec string (bech32 or hex) to be used for identity creation.
|
||||
pub fn resolve_identity(
|
||||
config: &Config,
|
||||
loaded_paths: &[PathBuf],
|
||||
) -> Result<ResolvedIdentity, ConfigError> {
|
||||
use crate::encode_nsec;
|
||||
|
||||
// Explicit nsec in config always wins
|
||||
if let Some(nsec) = &config.node.identity.nsec {
|
||||
return Ok(ResolvedIdentity {
|
||||
nsec: nsec.clone(),
|
||||
source: IdentitySource::Config,
|
||||
});
|
||||
}
|
||||
|
||||
// Determine key file directory from loaded config paths
|
||||
let config_ref = if let Some(path) = loaded_paths.last() {
|
||||
path.clone()
|
||||
} else {
|
||||
Config::search_paths()
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| PathBuf::from("./fips.yaml"))
|
||||
};
|
||||
let key_path = key_file_path(&config_ref);
|
||||
let pub_path = pub_file_path(&config_ref);
|
||||
|
||||
if config.node.identity.persistent {
|
||||
// Persistent mode: load existing key file or generate-and-persist
|
||||
if key_path.exists() {
|
||||
let nsec = read_key_file(&key_path)?;
|
||||
let identity = Identity::from_secret_str(&nsec)?;
|
||||
let _ = write_pub_file(&pub_path, &identity.npub());
|
||||
return Ok(ResolvedIdentity {
|
||||
nsec,
|
||||
source: IdentitySource::KeyFile(key_path),
|
||||
});
|
||||
}
|
||||
|
||||
// No key file yet — generate and persist
|
||||
let identity = Identity::generate();
|
||||
let nsec = encode_nsec(&identity.keypair().secret_key());
|
||||
let npub = identity.npub();
|
||||
|
||||
if let Some(parent) = key_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
match write_key_file(&key_path, &nsec) {
|
||||
Ok(()) => {
|
||||
let _ = write_pub_file(&pub_path, &npub);
|
||||
Ok(ResolvedIdentity {
|
||||
nsec,
|
||||
source: IdentitySource::Generated(key_path),
|
||||
})
|
||||
}
|
||||
Err(_) => Ok(ResolvedIdentity {
|
||||
nsec,
|
||||
source: IdentitySource::Ephemeral,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
// Ephemeral mode (default): fresh keypair every start, write key files
|
||||
// for operator visibility
|
||||
let identity = Identity::generate();
|
||||
let nsec = encode_nsec(&identity.keypair().secret_key());
|
||||
let npub = identity.npub();
|
||||
|
||||
if let Some(parent) = key_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
let _ = write_key_file(&key_path, &nsec);
|
||||
let _ = write_pub_file(&pub_path, &npub);
|
||||
|
||||
Ok(ResolvedIdentity {
|
||||
nsec,
|
||||
source: IdentitySource::Ephemeral,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of identity resolution.
|
||||
pub struct ResolvedIdentity {
|
||||
/// The nsec string (bech32 or hex) for creating an Identity.
|
||||
pub nsec: String,
|
||||
/// Where the identity came from.
|
||||
pub source: IdentitySource,
|
||||
}
|
||||
|
||||
/// Where a resolved identity originated.
|
||||
pub enum IdentitySource {
|
||||
/// From explicit nsec in config file.
|
||||
Config,
|
||||
/// Loaded from a persistent key file.
|
||||
KeyFile(PathBuf),
|
||||
/// Generated and saved to a new key file.
|
||||
Generated(PathBuf),
|
||||
/// Generated but could not be persisted.
|
||||
Ephemeral,
|
||||
}
|
||||
|
||||
/// Errors that can occur during configuration loading.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
@@ -53,6 +265,15 @@ pub enum ConfigError {
|
||||
source: serde_yaml::Error,
|
||||
},
|
||||
|
||||
#[error("key file is empty: {path}")]
|
||||
EmptyKeyFile { path: PathBuf },
|
||||
|
||||
#[error("failed to write key file {path}: {source}")]
|
||||
WriteKeyFile {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("identity error: {0}")]
|
||||
Identity(#[from] IdentityError),
|
||||
}
|
||||
@@ -64,6 +285,12 @@ pub struct IdentityConfig {
|
||||
/// If not specified, a new keypair will be generated.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nsec: Option<String>,
|
||||
|
||||
/// Whether to persist the identity across restarts (`node.identity.persistent`).
|
||||
/// When false (default), a fresh ephemeral keypair is generated each start.
|
||||
/// When true, the key file is reused across restarts.
|
||||
#[serde(default)]
|
||||
pub persistent: bool,
|
||||
}
|
||||
|
||||
/// Root configuration structure.
|
||||
@@ -172,6 +399,9 @@ impl Config {
|
||||
if other.node.identity.nsec.is_some() {
|
||||
self.node.identity.nsec = other.node.identity.nsec;
|
||||
}
|
||||
if other.node.identity.persistent {
|
||||
self.node.identity.persistent = true;
|
||||
}
|
||||
// Merge node.leaf_only
|
||||
if other.node.leaf_only {
|
||||
self.node.leaf_only = true;
|
||||
@@ -446,6 +676,179 @@ node:
|
||||
assert!(yaml.contains("test_nsec"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_file_write_read_roundtrip() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
|
||||
let identity = crate::Identity::generate();
|
||||
let nsec = crate::encode_nsec(&identity.keypair().secret_key());
|
||||
|
||||
write_key_file(&key_path, &nsec).unwrap();
|
||||
|
||||
let loaded_nsec = read_key_file(&key_path).unwrap();
|
||||
assert_eq!(loaded_nsec, nsec);
|
||||
|
||||
// Verify the loaded nsec produces the same identity
|
||||
let loaded_identity = crate::Identity::from_secret_str(&loaded_nsec).unwrap();
|
||||
assert_eq!(loaded_identity.npub(), identity.npub());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_file_permissions() {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
|
||||
write_key_file(&key_path, "nsec1test").unwrap();
|
||||
|
||||
let metadata = fs::metadata(&key_path).unwrap();
|
||||
assert_eq!(metadata.mode() & 0o777, 0o600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pub_file_permissions() {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let pub_path = temp_dir.path().join("fips.pub");
|
||||
|
||||
write_pub_file(&pub_path, "npub1test").unwrap();
|
||||
|
||||
let metadata = fs::metadata(&pub_path).unwrap();
|
||||
assert_eq!(metadata.mode() & 0o777, 0o644);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_file_empty_error() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
|
||||
fs::write(&key_path, "").unwrap();
|
||||
|
||||
let result = read_key_file(&key_path);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_file_whitespace_trimmed() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
|
||||
fs::write(&key_path, " nsec1test \n").unwrap();
|
||||
|
||||
let nsec = read_key_file(&key_path).unwrap();
|
||||
assert_eq!(nsec, "nsec1test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_file_path_derivation() {
|
||||
let config_path = PathBuf::from("/etc/fips/fips.yaml");
|
||||
assert_eq!(key_file_path(&config_path), PathBuf::from("/etc/fips/fips.key"));
|
||||
assert_eq!(pub_file_path(&config_path), PathBuf::from("/etc/fips/fips.pub"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_identity_from_config() {
|
||||
let mut config = Config::new();
|
||||
config.node.identity.nsec = Some(
|
||||
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string(),
|
||||
);
|
||||
|
||||
let resolved = resolve_identity(&config, &[]).unwrap();
|
||||
assert!(matches!(resolved.source, IdentitySource::Config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_identity_ephemeral_by_default() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
|
||||
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
assert!(!config.node.identity.persistent);
|
||||
|
||||
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
|
||||
assert!(matches!(resolved.source, IdentitySource::Ephemeral));
|
||||
|
||||
// Key files should still be written for operator visibility
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
let pub_path = temp_dir.path().join("fips.pub");
|
||||
assert!(key_path.exists());
|
||||
assert!(pub_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_identity_ephemeral_changes_each_call() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
|
||||
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
let first = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
|
||||
let second = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
|
||||
|
||||
// Each call generates a different key
|
||||
assert_ne!(first.nsec, second.nsec);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_identity_persistent_from_key_file() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
|
||||
fs::write(
|
||||
&config_path,
|
||||
"node:\n identity:\n persistent: true\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write a key file
|
||||
let identity = crate::Identity::generate();
|
||||
let nsec = crate::encode_nsec(&identity.keypair().secret_key());
|
||||
write_key_file(&key_path, &nsec).unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
assert!(config.node.identity.persistent);
|
||||
|
||||
let resolved = resolve_identity(&config, &[config_path]).unwrap();
|
||||
assert!(matches!(resolved.source, IdentitySource::KeyFile(_)));
|
||||
assert_eq!(resolved.nsec, nsec);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_identity_persistent_generates_and_persists() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
|
||||
fs::write(
|
||||
&config_path,
|
||||
"node:\n identity:\n persistent: true\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
|
||||
|
||||
assert!(matches!(resolved.source, IdentitySource::Generated(_)));
|
||||
|
||||
// Key file and pub file should now exist
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
let pub_path = temp_dir.path().join("fips.pub");
|
||||
assert!(key_path.exists());
|
||||
assert!(pub_path.exists());
|
||||
|
||||
// Second resolve should load from key file (not generate new)
|
||||
let resolved2 = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
|
||||
assert!(matches!(resolved2.source, IdentitySource::KeyFile(_)));
|
||||
assert_eq!(resolved.nsec, resolved2.nsec);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_yaml_empty_nsec_omitted() {
|
||||
let config = Config::new();
|
||||
|
||||
Reference in New Issue
Block a user