Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
f8c1453
Initial version of keyset.
Philip-NLnetLabs Apr 17, 2025
3621409
Now with keyset.rs.
Philip-NLnetLabs Apr 17, 2025
88c45bd
Fix Cargo.toml
Philip-NLnetLabs Apr 17, 2025
2ee5528
The CDS/CDNSKEY RRsets also need to be signed. Cron command.
Philip-NLnetLabs Apr 23, 2025
54fd380
Display *-generate-params as *-algorithm in show.
Philip-NLnetLabs May 6, 2025
df5ef0e
Avoid starting a KSK keyroll before init.
Philip-NLnetLabs May 22, 2025
97e218b
Make sure key tags are unique.
Philip-NLnetLabs May 28, 2025
3583c32
Support for algorithm rolls.
Philip-NLnetLabs Jun 12, 2025
19c2ff1
Merge branch 'main' into keyset
Philip-NLnetLabs Jun 12, 2025
9328445
Fix up merge.
Philip-NLnetLabs Jun 12, 2025
5d946be
Remove unneeded format!.
Philip-NLnetLabs Jun 12, 2025
174f31e
Merge branch 'main' into keyset
Philip-NLnetLabs Jun 12, 2025
2129fc2
Store key references are file URLs.
Philip-NLnetLabs Jun 17, 2025
688aada
More detailed error messages.
Philip-NLnetLabs Jun 18, 2025
7977bf7
More URL handling.
Philip-NLnetLabs Jun 19, 2025
75ce06f
Add support for cron_next.
Philip-NLnetLabs Jun 24, 2025
559f00e
Use clap to parse keyset subcommands (#100)
Philip-NLnetLabs Jul 11, 2025
ba81d62
Merge branch 'main' into keyset
Philip-NLnetLabs Jul 14, 2025
018527f
Add support for the KMIP cryptographic backend. (#99)
ximon18 Aug 25, 2025
6fad119
Add support for automatic key rolls (#108)
Philip-NLnetLabs Aug 29, 2025
df48644
Merge branch 'main' into keyset
Philip-NLnetLabs Sep 2, 2025
1f8f259
Restore white space.
Philip-NLnetLabs Sep 2, 2025
5bf2f3f
Getting ready to publish an 0.1.0-rc2 version of dnst with keyset sup…
ximon18 Sep 3, 2025
64b9ce9
Add missed Cargo.lock change due to rc2 version bump.
ximon18 Sep 3, 2025
1d6b2f2
Mark keyset as experimental.
ximon18 Sep 3, 2025
6a1e4cb
Extend O/S's supported for packaging.
ximon18 Sep 3, 2025
f50b657
Test missing/incorrect O/S variants.
ximon18 Sep 3, 2025
4f11b90
Permit KMIP servers to be added in an inactive state. (#122)
ximon18 Sep 3, 2025
e8dd8e2
Delete errant character that broke the YML syntax.
ximon18 Sep 3, 2025
c19673f
Keyset import (#121)
Philip-NLnetLabs Sep 5, 2025
2c18fac
Merge branch 'main' into keyset
Philip-NLnetLabs Sep 5, 2025
913ff60
Update Cargo.lock.
Philip-NLnetLabs Sep 5, 2025
c2acca2
Keyset status (#124)
Philip-NLnetLabs Sep 8, 2025
dc5a619
Merge branch 'main' into keyset
Philip-NLnetLabs Sep 8, 2025
96c94f1
Fallout from merging main: more map_err.
Philip-NLnetLabs Sep 8, 2025
dd78a25
Add support for imported keys to Init. (#126)
Philip-NLnetLabs Sep 9, 2025
3e6f79e
Implement the set default-ttl command. (#127)
Philip-NLnetLabs Sep 16, 2025
7d2cece
Create parent directories for config and state files. (#129)
Philip-NLnetLabs Sep 23, 2025
c62a097
Update 'Cargo.lock'
bal-e Sep 28, 2025
67b99a1
[keyset] Don't fail on repeated 'init'
bal-e Sep 29, 2025
2f7c2f5
Replace `Keys(s)` with `key(s)`. (#130)
ximon18 Oct 1, 2025
be7e53e
Revert "[keyset] Don't fail on repeated 'init'"
Philip-NLnetLabs Oct 1, 2025
cbd7a7b
Removed unused import.
Philip-NLnetLabs Oct 1, 2025
8f31bfc
Keyset man (#128)
Philip-NLnetLabs Oct 3, 2025
2f14623
Package as cascade-dnst instead of dnst. (#131)
ximon18 Oct 4, 2025
a4e9c65
dnst keyset man page tweaks. (#133)
ximon18 Oct 4, 2025
1184e35
Fix typo in docs
mozzieongit Oct 6, 2025
fb17d93
Fix error message saying .pub expected
mozzieongit Oct 9, 2025
ce21283
Introduce a 'WorkSpace' object to keep the current working state of k…
Philip-NLnetLabs Nov 3, 2025
63c3e7f
Typo correction.
ximon18 Nov 11, 2025
bbc1551
Atomic updates for the config and state files and locking of the conf…
Philip-NLnetLabs Nov 14, 2025
38b3b53
Merge branch 'keyset' into keyset-improvements
Philip-NLnetLabs Nov 14, 2025
f00078a
Fallout from merging.
Philip-NLnetLabs Nov 14, 2025
60da418
Switch to the crypto-and-keyset-fixes branch in domain for the time b…
Philip-NLnetLabs Nov 18, 2025
5e52e61
Update lock file.
Philip-NLnetLabs Nov 18, 2025
07d5bfe
Bump Rust version to 1.85 because domain is at 1.85.0.
Philip-NLnetLabs Nov 18, 2025
c53687a
Update TODO section.
Philip-NLnetLabs Nov 19, 2025
ef60307
Switch to domain-kmip. (#142)
Philip-NLnetLabs Nov 26, 2025
535f572
Merge branch 'keyset-improvements' into keyset-locking
Philip-NLnetLabs Nov 26, 2025
c84e21c
Cargo fmt.
Philip-NLnetLabs Nov 26, 2025
c21a9db
Switch to same-file.
Philip-NLnetLabs Nov 26, 2025
0a13d74
Minor typo correction in man page sources.
ximon18 Dec 8, 2025
48d1eb2
Minor typo correction in man page sources.
ximon18 Dec 8, 2025
4044fe5
Fix RST syntax error in man page source.
ximon18 Dec 8, 2025
ac75810
Switch to main branch of domain-kmip.
Philip-NLnetLabs Dec 12, 2025
4f6f762
Clippy.
Philip-NLnetLabs Dec 15, 2025
7accc03
Bump Rust version to 1.88 for kmip-protocol.
Philip-NLnetLabs Dec 16, 2025
ceca925
Merge branch 'main' into keyset
Philip-NLnetLabs Jan 22, 2026
7d37971
Merge branch 'keyset' into keyset-improvements
Philip-NLnetLabs Jan 22, 2026
dd24009
Introduce a 'WorkSpace' object to keep the current working state of k…
Philip-NLnetLabs Jan 22, 2026
ce572cd
Undo Cascade-related changes.
Philip-NLnetLabs Jan 23, 2026
d20b476
Update manual pages.
Philip-NLnetLabs Jan 23, 2026
8bfef3b
Merge branch 'main' into keyset
Philip-NLnetLabs Jan 23, 2026
7337a3a
Update man pages.
Philip-NLnetLabs Jan 23, 2026
953cd96
Merge branch 'main' into keyset
Philip-NLnetLabs Jan 23, 2026
212a8ff
Merge branch 'keyset' into keyset-improvements
Philip-NLnetLabs Jan 23, 2026
84cca6a
Merge branch 'keyset-improvements' into keyset-locking
Philip-NLnetLabs Jan 23, 2026
9d072a5
Merge branch 'main' into keyset-locking
Philip-NLnetLabs Feb 27, 2026
570220a
Update src/commands/keyset/cmd.rs
Philip-NLnetLabs Mar 2, 2026
9301b12
Review feedback.
Philip-NLnetLabs Mar 2, 2026
10d2fa6
Extract common code in write_config and write_state.
Philip-NLnetLabs Mar 2, 2026
5976442
write_locked_file -> file_with_write_lock
Philip-NLnetLabs Mar 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ tracing = "0.1.41"
tracing-subscriber = "0.3.19"
url = "2.5.4"
futures = "0.3.31"
fs2 = "0.4.3"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a good choice of dependency. What were your reasons for selecting it?
I ask because:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave it at fs2 for now. If fs2 gets less popular or picks up security issues then we can switch. The interface of fd-lock looks scary.

same-file = "1.0.6"
supports-color = "3.0.2"

[dev-dependencies]
Expand Down
151 changes: 105 additions & 46 deletions src/commands/keyset/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,17 @@ use domain_kmip as kmip;
use domain_kmip::dep::kmip::client::pool::SyncConnPool;
#[cfg(feature = "kmip")]
use domain_kmip::KeyUrl;
use fs2::FileExt;
use futures::future::join_all;
use jiff::{Span, SpanRelativeTo};
use same_file::Handle;
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::convert::From;
use std::ffi::OsStr;
use std::fmt::{Debug, Display, Formatter};
use std::fs::{create_dir_all, remove_file, File};
use std::fs::{create_dir_all, remove_file, rename, File};
use std::io::{self, Write};
use std::net::{IpAddr, SocketAddr};
use std::path::{absolute, Path, PathBuf};
Expand All @@ -73,6 +75,9 @@ use super::kmip::{format_key_label, kmip_command, KmipCommands, KmipState};
/// with the key tags of existing keys.
const MAX_KEY_TAG_TRIES: u8 = 10;

/// Number of times to try locking a file.
const MAX_FILE_LOCK_TRIES: u8 = 10;

/// Wait this amount before retrying for network errors, DNS errors, etc.
const DEFAULT_WAIT: Duration = Duration::from_secs(10 * 60);

Expand Down Expand Up @@ -629,6 +634,9 @@ struct WorkSpace {
/// Whether the command to update DS records has to be executed.
run_update_ds_command: bool,

/// Store the locked config file to avoid accidental unlocking.
_locked_config_file: Option<File>,

#[cfg(feature = "kmip")]
/// The current set of KMIP server pools.
pools: HashMap<String, SyncConnPool>,
Expand Down Expand Up @@ -711,32 +719,26 @@ impl Keyset {
)
})?;

let json = serde_json::to_string_pretty(&kss).expect("should not fail");
let mut file = File::create(&state_file)
.map_err(|e| format!("unable to create file {}: {e}", state_file.display()))?;
write!(file, "{json}")
.map_err(|e| format!("unable to write to file {}: {e}", state_file.display()))?;
let ws = WorkSpace {
config: ksc,
state: kss,
config_changed: false,
state_changed: false,
run_update_ds_command: false,
_locked_config_file: None,
#[cfg(feature = "kmip")]
pools: HashMap::new(),
};

ws.write_state()?;
ws.write_config(&self.keyset_conf)?;

let json = serde_json::to_string_pretty(&ksc).expect("should not fail");
let mut file = File::create(&self.keyset_conf).map_err(|e| {
format!("unable to create file {}: {e}", self.keyset_conf.display())
})?;
write!(file, "{json}").map_err(|e| {
format!(
"unable to write to file {}: {e}",
self.keyset_conf.display()
)
})?;
return Ok(());
}

let file = File::open(self.keyset_conf.clone()).map_err(|e| {
format!(
"unable to open config file {}: {e}",
self.keyset_conf.display()
)
})?;
let ksc: KeySetConfig = serde_json::from_reader(file)
let config_file = file_with_write_lock(&self.keyset_conf)?;

let ksc: KeySetConfig = serde_json::from_reader(&config_file)
.map_err(|e| format!("error loading {:?}: {e}\n", self.keyset_conf))?;
let file = File::open(ksc.state_file.clone()).map_err(|e| {
format!(
Expand All @@ -753,6 +755,7 @@ impl Keyset {
config_changed: false,
state_changed: false,
run_update_ds_command: false,
_locked_config_file: Some(config_file),
#[cfg(feature = "kmip")]
pools: HashMap::new(),
};
Expand Down Expand Up @@ -1556,31 +1559,10 @@ impl Keyset {
ws.state_changed = true;
}
if ws.config_changed {
let json = serde_json::to_string_pretty(&ws.config).expect("should not fail");
let mut file = File::create(&self.keyset_conf).map_err(|e| {
format!("unable to create file {}: {e}", self.keyset_conf.display())
})?;
write!(file, "{json}").map_err(|e| {
format!(
"unable to write to file {}: {e}",
self.keyset_conf.display()
)
})?;
ws.write_config(&self.keyset_conf)?;
}
if ws.state_changed {
let json = serde_json::to_string_pretty(&ws.state).expect("should not fail");
let mut file = File::create(&ws.config.state_file).map_err(|e| {
format!(
"unable to create file {}: {e}",
ws.config.state_file.display()
)
})?;
write!(file, "{json}").map_err(|e| {
format!(
"unable to write to file {}: {e}",
ws.config.state_file.display()
)
})?;
ws.write_state()?;
}

// Now check if we need to run the update_ds_command. Make sure that
Expand Down Expand Up @@ -3760,6 +3742,43 @@ impl WorkSpace {
let new_algs = HashSet::from([self.config.algorithm.to_generate_params().algorithm()]);
curr_algs != new_algs
}

/// Write config to a file.
fn write_config(&self, keyset_conf: &PathBuf) -> Result<(), Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and fn write_state() look almost identical, I would suggest factoring their core logic out into a single helper function, especially as they both contain the same comment and work around for lacking -fn add_extension() support.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

let json = serde_json::to_string_pretty(&self.config).expect("should not fail");
Self::write_to_new_and_rename(&json, keyset_conf)
}

/// Write state to a file.
fn write_state(&self) -> Result<(), Error> {
let json = serde_json::to_string_pretty(&self.state).expect("should not fail");
Self::write_to_new_and_rename(&json, &self.config.state_file)
}

/// First write to a new filename and then rename to make sure that
/// changes are atomic.
fn write_to_new_and_rename(json: &str, filename: &PathBuf) -> Result<(), Error> {
let mut filename_new = filename.clone();
// It would be nice to use add_extension here, but it is only in
// Rust 1.91.0 and above. Use strings instead.
// if !state_file_new.add_extension("new") {
// return Err(format!("unable to add extension 'new' to {}",
// ws.config.state_file.display()).into());
// }
filename_new.as_mut_os_string().push(".new");
let mut file = File::create(&filename_new)
.map_err(|e| format!("unable to create file {}: {e}", filename_new.display()))?;
write!(file, "{json}")
.map_err(|e| format!("unable to write to file {}: {e}", filename_new.display()))?;
rename(&filename_new, filename).map_err(|e| {
format!(
"unable to rename {} to {}: {e}",
filename_new.display(),
filename.display()
)
})?;
Ok(())
}
}

/// Create CDS and CDNSKEY RRsets.
Expand Down Expand Up @@ -5658,6 +5677,46 @@ fn show_automatic_roll_state(
}
}

/// Open filename, get an exclusive lock and return the open file.
///
/// Assume changes are saved by creating a new file and renaming. After
/// locking the file, the function has to check if the locked file is the
/// same as the current file under that name.
fn file_with_write_lock(filename: &PathBuf) -> Result<File, Error> {
// The config file is updated by writing to a new file and then renaming.
// We might have locked the old file. Check. Try a number of times and
// then give up. Lock contention is expected to be low.
for _try in 0..MAX_FILE_LOCK_TRIES {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it okay to retry lock acquisition with no delay between attempts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will lead to the thundering herd problem. But concurrent access to keyset for a single domain is expected to be very low. We need to do something if we ever fail to get a lock within MAX_FILE_LOCK_TRIES.

let file = File::open(filename)
.map_err(|e| format!("unable to open file {}: {e}", filename.display()))?;

file.lock_exclusive()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the documentation this call will block if the file is currently locked, is that desirable or would it be better to call try_lock_exclusive() instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this PR the goal is to block. Maybe the UI needs to be improved in the future.

.map_err(|e| format!("unable to lock {}: {e}", filename.display()))?;

let file_clone = file
.try_clone()
.map_err(|e| format!("unable to clone locked file {}: {e}", filename.display()))?;
let locked_file_handle = Handle::from_file(file_clone).map_err(|e| {
format!(
"Unable to get handle from locked file {}: {e}",
filename.display()
)
})?;
let current_file_handle = Handle::from_path(filename)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You open a file by path, lock it and get its handle, then get another handle for the same path and compare the two handles. Why? What is this handle comparison achieving/intended to do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is explained by the comment at the top of the function. Maybe it needs more text. The idea is that because the config is saved by creating a new file and then renaming that file, it is possible to endup with a lock on the old config file that no longer exists. So check that we have locked to current config file.

Copy link
Member

@ximon18 ximon18 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only call write_locked_file() once, so any rename that happens within your code when the config file is saved won't be detected later by this logic, at least currently. So is this for the some future potential second call to this function after saving/renaming of the config file?

Also, when this test fails you call continue so you go round the loop again, dropping the file you just opened and the lock you just acquired, and try re-opening the file by the same filename as you just tried, even though that succeeded. I don't understand what this achieves.

If this function were called while a second concurrent invocation of dnst keyset was busy saving the config file, one would be able to open the file then block until the lock can be acquired, is this the issue, that you would then be possibly be opening the old FD? (though what happens to that FD during the rename/once the rename completes, is it still valid?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a rename, any processes that run keyset and that were trying to lock the config file now try to lock the wrong file. So they need to start again. The logic of keyset is that it gets a lock on the config file, and then saves the state file if it changed and saves the config file if it changed. There will not be a second call to lock the config file in a single run of keyset.

.map_err(|e| format!("Unable to get handle from file {}: {e}", filename.display()))?;

if locked_file_handle != current_file_handle {
continue;
}
return Ok(file);
}
Err(format!(
"unable to lock {} after {MAX_FILE_LOCK_TRIES} tries",
filename.display()
)
.into())
}

/// Helper function for serde.
///
/// Return the default autoremove delay.
Expand Down