mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
Stop writing the identity key file in ephemeral mode
An ephemeral node wrote the private key of an identity it discards on every restart to fips.key, and overwrote any key already at that path, including an operator's key in the case where persistent: true was forgotten. It now writes only fips.pub, so the running npub stays visible, and holds the private key in memory only. A fips.key found at an ephemeral start is moved aside to fips.key.unused with a warning rather than used or overwritten, so an operator's key is recoverable and a stale key from an earlier release stops being read by fipsctl address. If that name is already taken or the rename fails, the file is left in place, a warning says so, and the start continues. Persistent and explicit-nsec identities are unchanged. The keygen note, the Windows service installer's closing note, and the documentation that described the old write are corrected, including the persistent identity how-to, which told operators to start once in ephemeral mode and then pin the key it wrote.
This commit is contained in:
@@ -35,19 +35,12 @@ nodes, and tests where you actively want a fresh identity per run.
|
||||
|
||||
The Debian/Ubuntu `.deb` and the Arch `fips` AUR package both ship a
|
||||
default `/etc/fips/fips.yaml` with `node.identity.persistent` left as
|
||||
the upstream default (false), so the daemon writes a fresh keypair to
|
||||
`/etc/fips/fips.{key,pub}` on every start until you set
|
||||
`persistent: true`. To pin the current keypair:
|
||||
the upstream default (false). In that mode the daemon generates a
|
||||
fresh keypair on every start, holds the private key only in memory,
|
||||
and writes only `/etc/fips/fips.pub`. To give the node a stable
|
||||
identity:
|
||||
|
||||
1. Install the package and start the daemon once so it generates
|
||||
`fips.key` / `fips.pub`:
|
||||
|
||||
```sh
|
||||
sudo systemctl start fips
|
||||
sudo systemctl status fips # confirm it came up
|
||||
```
|
||||
|
||||
2. Edit `/etc/fips/fips.yaml` and set:
|
||||
1. Install the package, then edit `/etc/fips/fips.yaml` and set:
|
||||
|
||||
```yaml
|
||||
node:
|
||||
@@ -55,22 +48,31 @@ the upstream default (false), so the daemon writes a fresh keypair to
|
||||
persistent: true
|
||||
```
|
||||
|
||||
3. Restart the daemon and verify the identity is reused:
|
||||
2. Start or restart the daemon. On this first persistent start it
|
||||
generates a keypair and saves it to `/etc/fips/fips.key`:
|
||||
|
||||
```sh
|
||||
sudo systemctl restart fips
|
||||
sudo systemctl status fips # confirm it came up
|
||||
```
|
||||
|
||||
3. Verify the identity:
|
||||
|
||||
```sh
|
||||
fipsctl show status | grep -E '"npub"|"node_addr"'
|
||||
cat /etc/fips/fips.pub
|
||||
```
|
||||
|
||||
The npub printed by `fipsctl show status` should match
|
||||
`/etc/fips/fips.pub` and remain stable across subsequent restarts.
|
||||
`/etc/fips/fips.pub`. If the daemon was already running
|
||||
ephemeral, the npub changes once at this restart and is stable
|
||||
from then on.
|
||||
|
||||
The package's `postinst` script does **not** generate the keypair —
|
||||
the daemon does, on first start. This means the keypair is only
|
||||
present after the first successful daemon start. If the daemon never
|
||||
came up cleanly (config error, permission problem), the key files
|
||||
will be missing.
|
||||
The package's `postinst` script does **not** generate the keypair.
|
||||
The first successful daemon start with `persistent: true` does, so
|
||||
`fips.key` is only present after that start. If the daemon never came
|
||||
up cleanly (config error, permission problem), the key file will be
|
||||
missing.
|
||||
|
||||
### macOS note
|
||||
|
||||
|
||||
@@ -76,16 +76,18 @@ first, then `/usr/local/etc/fips`, so the packaged file wins over a
|
||||
leftover `/etc/fips` copy from an earlier install. Windows likewise
|
||||
probes `\etc\fips` on the current drive, then `C:\ProgramData\fips`.
|
||||
|
||||
Adjacent to the highest-priority config file the daemon reads (or
|
||||
writes, on first start) the identity files:
|
||||
Adjacent to the highest-priority config file the daemon keeps the
|
||||
identity files:
|
||||
|
||||
| File | Mode | Purpose |
|
||||
| ---- | ---- | ------- |
|
||||
| `fips.key` | `0600` | Bech32 nsec for the persistent identity (Unix; on Windows the file takes its directory's ACL, which `install-service.ps1` restricts to SYSTEM and Administrators). |
|
||||
| `fips.pub` | `0644` | Bech32 npub corresponding to `fips.key`. |
|
||||
| `fips.key` | `0600` | Bech32 nsec for the persistent identity, written only in persistent mode (Unix; on Windows the file takes its directory's ACL, which `install-service.ps1` restricts to SYSTEM and Administrators). |
|
||||
| `fips.pub` | `0644` | Bech32 npub of the running identity, written on every start. In persistent mode it corresponds to `fips.key`. |
|
||||
|
||||
When `node.identity.persistent` is `false` (the default), a fresh
|
||||
keypair is written to these files on every start.
|
||||
keypair is generated on every start and only `fips.pub` is written.
|
||||
A `fips.key` found there is moved aside to `fips.key.unused` and a
|
||||
warning is logged.
|
||||
|
||||
On Windows the service writes its log to `C:\ProgramData\fips\fips.log`,
|
||||
rolled at 10 MiB with four old files kept; a foreground run logs to the
|
||||
|
||||
@@ -108,8 +108,10 @@ Identity resolution follows a three-tier priority:
|
||||
3. **Ephemeral** — when `persistent: false` (default) and no `nsec`, generates a fresh
|
||||
keypair on each start
|
||||
|
||||
Key files (`fips.key` with mode 0600, `fips.pub` with mode 0644) are written adjacent
|
||||
to the highest-priority config file for operator visibility, even in ephemeral mode.
|
||||
`fips.pub` (mode 0644) is written adjacent to the highest-priority config file
|
||||
on every start. `fips.key` (mode 0600) is written only in persistent mode. In
|
||||
ephemeral mode a `fips.key` found at startup is moved aside to
|
||||
`fips.key.unused` with a warning.
|
||||
|
||||
### General
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ operator-editable runtime state:
|
||||
| Path | Purpose | Writable | Seeded from |
|
||||
|---|---|---|---|
|
||||
| `/var/lib/fips/fips.yaml` | Main config | yes | `services.fips.configFile` (first run only) |
|
||||
| `/var/lib/fips/fips.key` | Node identity (private) | yes | generated by fips on first start |
|
||||
| `/var/lib/fips/fips.pub` | Node identity (public) | yes | generated by fips on first start |
|
||||
| `/var/lib/fips/fips.key` | Node identity (private) | yes | generated by fips on first start with `persistent: true` |
|
||||
| `/var/lib/fips/fips.pub` | Node identity (public) | yes | written by fips on every start |
|
||||
| `/etc/fips/hosts` | Static hostname → npub map | yes | shipped `hosts` (first run only) |
|
||||
| `/etc/fips/peers.allow` | Peer allowlist (ACL) | yes | operator-created |
|
||||
| `/etc/fips/peers.deny` | Peer denylist (ACL) | yes | operator-created |
|
||||
|
||||
@@ -17,8 +17,8 @@ sudo ./install.sh
|
||||
| fipstop (TUI) | /usr/local/bin/fipstop |
|
||||
| fips-gateway (LAN bridge) | /usr/local/bin/fips-gateway |
|
||||
| Configuration | /etc/fips/fips.yaml |
|
||||
| Identity key | /etc/fips/fips.key (auto-generated) |
|
||||
| Public key | /etc/fips/fips.pub (auto-generated) |
|
||||
| Identity key | /etc/fips/fips.key (generated on first start with `persistent: true`) |
|
||||
| Public key | /etc/fips/fips.pub (written on every start) |
|
||||
| Hosts file | /etc/fips/hosts |
|
||||
| Firewall baseline | /etc/fips/fips.nft |
|
||||
| Firewall drop-in directory | /etc/fips/fips.d/ |
|
||||
|
||||
@@ -101,9 +101,9 @@ Control Socket:
|
||||
|
||||
Configuration:
|
||||
The service reads C:\ProgramData\fips\fips.yaml, where
|
||||
install-service.ps1 puts it, and keeps fips.key, hosts,
|
||||
peers.allow and peers.deny beside it. Edit fips.yaml there
|
||||
before starting the service.
|
||||
install-service.ps1 puts it, and keeps hosts, peers.allow,
|
||||
peers.deny and, with node.identity.persistent: true, fips.key
|
||||
beside it. Edit fips.yaml there before starting the service.
|
||||
|
||||
install-service.ps1 restricts C:\ProgramData\fips to SYSTEM
|
||||
and Administrators before writing into it. Reading or editing
|
||||
|
||||
+2
-1
@@ -484,7 +484,8 @@ mod service {
|
||||
"Configuration: the service reads {}",
|
||||
dir.join("fips.yaml").display()
|
||||
);
|
||||
println!(" keep fips.key, hosts, peers.allow and peers.deny beside it.");
|
||||
println!(" keep hosts, peers.allow and peers.deny beside it, and fips.key");
|
||||
println!(" too when node.identity.persistent is true.");
|
||||
println!(
|
||||
"Logs: the service writes {}",
|
||||
dir.join("fips.log").display()
|
||||
|
||||
+6
-2
@@ -593,8 +593,12 @@ fn main() {
|
||||
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.");
|
||||
eprintln!(
|
||||
"NOTE: Set 'node.identity.persistent: true' in fips.yaml before the next daemon start."
|
||||
);
|
||||
eprintln!(
|
||||
" Without it the daemon does not use this key: it moves fips.key aside to fips.key.unused."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+265
-30
@@ -595,13 +595,60 @@ pub fn write_pub_file(path: &Path, npub: &str) -> Result<(), ConfigError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move a key file found at an ephemeral start to `<name>.unused` beside it,
|
||||
/// so it is neither used nor overwritten and stays recoverable.
|
||||
///
|
||||
/// Returns the new path when the file was moved. Does nothing when no file is
|
||||
/// at `key_path`; a dangling symlink counts as a file and is moved as a link.
|
||||
/// An existing file at the aside path is never replaced: the key is left in
|
||||
/// place and a warning says so, as it does when the rename fails. Neither case
|
||||
/// stops the start.
|
||||
fn retire_key(key_path: &Path) -> Option<PathBuf> {
|
||||
// symlink_metadata rather than exists: a dangling symlink at the key path
|
||||
// reports exists() == false but is still a file the operator put there.
|
||||
key_path.symlink_metadata().ok()?;
|
||||
|
||||
let mut name = key_path.file_name()?.to_os_string();
|
||||
name.push(".unused");
|
||||
let aside = key_path.with_file_name(name);
|
||||
|
||||
let failure = match aside.symlink_metadata() {
|
||||
Ok(_) => "a file already exists at the aside path".to_string(),
|
||||
Err(e) if e.kind() != std::io::ErrorKind::NotFound => e.to_string(),
|
||||
Err(_) => match std::fs::rename(key_path, &aside) {
|
||||
Ok(()) => {
|
||||
tracing::warn!(
|
||||
path = %key_path.display(),
|
||||
moved_to = %aside.display(),
|
||||
config_key = "node.identity.persistent",
|
||||
"An identity key file was found in ephemeral mode and moved aside, not used; \
|
||||
set node.identity.persistent: true and move it back to use it"
|
||||
);
|
||||
return Some(aside);
|
||||
}
|
||||
Err(e) => e.to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
path = %key_path.display(),
|
||||
aside = %aside.display(),
|
||||
error = %failure,
|
||||
config_key = "node.identity.persistent",
|
||||
"An identity key file was found in ephemeral mode and could not be moved aside; \
|
||||
it is not used; set node.identity.persistent: true to use it, or remove it"
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// every start. Only `fips.pub` is written, so the running npub is visible;
|
||||
/// the private key is never written. A `fips.key` already at the path is
|
||||
/// moved aside to `fips.key.unused` with a warning, not used or overwritten.
|
||||
///
|
||||
/// - **`persistent: true`**: use three-tier resolution:
|
||||
/// 1. Explicit nsec in config — highest priority
|
||||
@@ -737,8 +784,8 @@ pub fn resolve_identity(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ephemeral mode (default): fresh keypair every start, write key files
|
||||
// for operator visibility
|
||||
// Ephemeral mode (default): a fresh keypair every start, held only in
|
||||
// memory. Only the public key file is written.
|
||||
let identity = Identity::generate();
|
||||
// `keypair()` and `secret_key()` each hand back a whole private key
|
||||
// rather than a handle, so both temporaries are bound and erased.
|
||||
@@ -753,25 +800,8 @@ pub fn resolve_identity(
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
// symlink_metadata rather than exists: a dangling symlink at the key
|
||||
// path reports exists() == false but is still an existing file the
|
||||
// write is about to act on.
|
||||
if key_path.symlink_metadata().is_ok() {
|
||||
tracing::warn!(
|
||||
path = %key_path.display(),
|
||||
config_key = "node.identity.persistent",
|
||||
"An existing key file at this path is being replaced by a fresh ephemeral \
|
||||
identity; set node.identity.persistent: true to keep the existing identity"
|
||||
);
|
||||
}
|
||||
retire_key(&key_path);
|
||||
|
||||
if let Err(e) = write_key_file(&key_path, &nsec) {
|
||||
tracing::warn!(
|
||||
path = %key_path.display(),
|
||||
error = %e,
|
||||
"Failed to write the ephemeral key file"
|
||||
);
|
||||
}
|
||||
if let Err(e) = write_pub_file(&pub_path, &npub) {
|
||||
tracing::warn!(
|
||||
path = %pub_path.display(),
|
||||
@@ -2020,29 +2050,64 @@ node:
|
||||
assert_eq!(fs::read_to_string(&victim).unwrap(), "victim contents\n");
|
||||
}
|
||||
|
||||
/// The names in `dir`, sorted, so a test can assert on the whole directory.
|
||||
fn dir_names(dir: &Path) -> Vec<String> {
|
||||
let mut names: Vec<String> = fs::read_dir(dir)
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
|
||||
.collect();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
/// The npub a resolved identity runs as.
|
||||
fn resolved_npub(resolved: &ResolvedIdentity) -> String {
|
||||
crate::Identity::from_secret_str(&resolved.nsec)
|
||||
.unwrap()
|
||||
.npub()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ephemeral_over_existing_key_warns() {
|
||||
fn ephemeral_start_moves_an_existing_key_aside_intact() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
let aside_path = temp_dir.path().join("fips.key.unused");
|
||||
|
||||
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
|
||||
let identity = crate::Identity::generate();
|
||||
let existing = crate::encode_nsec(&identity.keypair().secret_key());
|
||||
write_key_file(&key_path, &existing).unwrap();
|
||||
let planted = fs::read(&key_path).unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
let (resolved, logs) =
|
||||
capture_logs(|| resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap());
|
||||
|
||||
assert_ne!(resolved.nsec, existing);
|
||||
assert!(
|
||||
key_path.symlink_metadata().is_err(),
|
||||
"the key file must be moved away from the path a persistent start reads"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(&aside_path).unwrap(),
|
||||
planted,
|
||||
"the key set aside must hold the planted bytes exactly"
|
||||
);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
assert_eq!(fs::metadata(&aside_path).unwrap().mode() & 0o777, 0o600);
|
||||
}
|
||||
let warnings = logs.warnings();
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains(&key_path.display().to_string())
|
||||
&& w.contains("node.identity.persistent")),
|
||||
"expected a warning naming the key path and the config key, got {warnings:?}"
|
||||
&& w.contains(&aside_path.display().to_string())
|
||||
&& w.contains("node.identity.persistent")
|
||||
&& w.contains("and moved aside, not used")),
|
||||
"expected the moved-aside warning naming both paths and the config key, got {warnings:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2052,6 +2117,7 @@ node:
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
let aside_path = temp_dir.path().join("fips.key.unused");
|
||||
let target = temp_dir.path().join("absent-target");
|
||||
|
||||
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
|
||||
@@ -2065,8 +2131,21 @@ node:
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains(&key_path.display().to_string())),
|
||||
"expected a warning naming the key path, got {warnings:?}"
|
||||
.any(|w| w.contains(&key_path.display().to_string())
|
||||
&& w.contains("and moved aside, not used")),
|
||||
"expected the moved-aside warning naming the key path, got {warnings:?}"
|
||||
);
|
||||
assert!(
|
||||
key_path.symlink_metadata().is_err(),
|
||||
"the dangling symlink must be moved away from the key path"
|
||||
);
|
||||
assert!(
|
||||
aside_path
|
||||
.symlink_metadata()
|
||||
.unwrap()
|
||||
.file_type()
|
||||
.is_symlink(),
|
||||
"the symlink itself must be what was moved aside, not a file written through it"
|
||||
);
|
||||
assert!(
|
||||
!target.exists(),
|
||||
@@ -2074,6 +2153,155 @@ node:
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ephemeral_start_leaves_a_key_in_place_when_the_aside_name_is_taken() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
let aside_path = temp_dir.path().join("fips.key.unused");
|
||||
|
||||
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
|
||||
let current = crate::encode_nsec(&crate::Identity::generate().keypair().secret_key());
|
||||
let earlier = crate::encode_nsec(&crate::Identity::generate().keypair().secret_key());
|
||||
write_key_file(&key_path, ¤t).unwrap();
|
||||
write_key_file(&aside_path, &earlier).unwrap();
|
||||
let key_bytes = fs::read(&key_path).unwrap();
|
||||
let aside_bytes = fs::read(&aside_path).unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
let (resolved, logs) =
|
||||
capture_logs(|| resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap());
|
||||
|
||||
assert!(matches!(resolved.source, IdentitySource::Ephemeral));
|
||||
assert_ne!(resolved.nsec, current);
|
||||
assert_eq!(
|
||||
fs::read(&key_path).unwrap(),
|
||||
key_bytes,
|
||||
"the key must be left in place, not overwritten"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(&aside_path).unwrap(),
|
||||
aside_bytes,
|
||||
"the file already at the aside path must not be replaced"
|
||||
);
|
||||
let warnings = logs.warnings();
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains(&key_path.display().to_string())
|
||||
&& w.contains(&aside_path.display().to_string())
|
||||
&& w.contains("node.identity.persistent")
|
||||
&& w.contains("could not be moved aside")),
|
||||
"expected the could-not-move warning naming both paths and the config key, got {warnings:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn ephemeral_start_proceeds_when_the_key_cannot_be_moved() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
/// Puts the directory's mode back when dropped, so a failing
|
||||
/// assertion does not leave a read-only directory behind that
|
||||
/// `TempDir` cannot remove.
|
||||
struct RestoreMode<'a>(&'a Path);
|
||||
impl Drop for RestoreMode<'_> {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::set_permissions(self.0, fs::Permissions::from_mode(0o755));
|
||||
}
|
||||
}
|
||||
|
||||
// Coverage gap: root bypasses directory permissions, so the rename
|
||||
// succeeds and this branch goes unexercised when the suite runs as
|
||||
// root. The aside-name-taken test still covers the same warning.
|
||||
if unsafe { libc::geteuid() } == 0 {
|
||||
eprintln!("skipped: running as root, which a read-only directory does not stop");
|
||||
return;
|
||||
}
|
||||
|
||||
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").unwrap();
|
||||
let existing = crate::encode_nsec(&crate::Identity::generate().keypair().secret_key());
|
||||
write_key_file(&key_path, &existing).unwrap();
|
||||
let planted = fs::read(&key_path).unwrap();
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
|
||||
fs::set_permissions(temp_dir.path(), fs::Permissions::from_mode(0o555)).unwrap();
|
||||
let _restore = RestoreMode(temp_dir.path());
|
||||
|
||||
let (resolved, logs) =
|
||||
capture_logs(|| resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap());
|
||||
|
||||
assert!(matches!(resolved.source, IdentitySource::Ephemeral));
|
||||
assert_ne!(resolved.nsec, existing);
|
||||
assert_eq!(
|
||||
fs::read(&key_path).unwrap(),
|
||||
planted,
|
||||
"a key that cannot be moved must be left as it was, not overwritten"
|
||||
);
|
||||
let warnings = logs.warnings();
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains(&key_path.display().to_string())
|
||||
&& w.contains("could not be moved aside")
|
||||
&& w.contains("os error")),
|
||||
"expected a warning naming the key path and the rename error, got {warnings:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ephemeral_restarts_never_leave_a_private_key_file() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
let pub_path = temp_dir.path().join("fips.pub");
|
||||
|
||||
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
|
||||
for start in 1..=3 {
|
||||
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
|
||||
assert_eq!(
|
||||
dir_names(temp_dir.path()),
|
||||
["fips.pub", "fips.yaml"],
|
||||
"after start {start} the directory must hold only the config and the public key"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(&pub_path).unwrap().trim(),
|
||||
resolved_npub(&resolved),
|
||||
"after start {start} fips.pub must name the running identity"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistent_start_ignores_a_key_set_aside() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
let key_path = temp_dir.path().join("fips.key");
|
||||
let aside_path = temp_dir.path().join("fips.key.unused");
|
||||
|
||||
fs::write(&config_path, "node:\n identity:\n persistent: true\n").unwrap();
|
||||
let earlier = crate::encode_nsec(&crate::Identity::generate().keypair().secret_key());
|
||||
write_key_file(&aside_path, &earlier).unwrap();
|
||||
let aside_bytes = fs::read(&aside_path).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(_)));
|
||||
assert_ne!(resolved.nsec, earlier);
|
||||
assert_eq!(read_key_file(&key_path).unwrap(), resolved.nsec);
|
||||
assert_eq!(
|
||||
fs::read(&aside_path).unwrap(),
|
||||
aside_bytes,
|
||||
"a persistent start must leave the key set aside untouched"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_persistent_permissive_key_warns() {
|
||||
@@ -2205,11 +2433,18 @@ node:
|
||||
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
|
||||
// Only the public key is written: an ephemeral private key lives in
|
||||
// memory and nowhere else.
|
||||
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());
|
||||
assert_eq!(
|
||||
key_path.symlink_metadata().unwrap_err().kind(),
|
||||
std::io::ErrorKind::NotFound
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(&pub_path).unwrap().trim(),
|
||||
resolved_npub(&resolved)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user