Files
NLnetLabs-domain/examples/keyset.rs
T
arya dradjicaandGitHub 08189fcc55 Switch to Rust 2024 (#645)
Now that our MSRV is past 1.85, we can switch to the 2024 edition. This
changes some minor behavior in the language and exposes new lints (both
from check and clippy). In particular, unsafe functions now require
unsafe blocks to call other unsafe functions; this is quite a nice
change as it exposes more places where safety comments are needed.
2026-06-01 14:53:22 +02:00

570 lines
17 KiB
Rust

//! Demonstrate the use of key sets.
use domain::base::Name;
use domain::base::iana::SecurityAlgorithm;
use domain::dnssec::sign::keys::keyset::{
Action, Available, Error, KeySet, KeyType, RollType, UnixTime,
};
use itertools::{Either, Itertools};
use std::env;
use std::fs::File;
use std::io::Write;
use std::process::exit;
use std::str::FromStr;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 3 {
// Should display a help message including example.
eprintln!(
"Subcommand required. Valid subcommands are:
init <domain>
addkey <key-type> <pubref> [<privref>]
deletekey <pubref>
start <roll-type>
propagation1-complete <roll-type> <ttl>
cache-expired1 <roll-type>
propagation2-complete <roll-type> <ttl>
cache-expired2 <roll-type>
done <roll-type>
actions <roll-type>
status
"
);
eprintln!();
eprintln!(
"Try for example the following commands:
keyset example-keyset.json init example.com
keyset example-keyset.json addkey ksk first-ksk.key
keyset example-keyset.json addkey zsk first-zsk.key first-zsk.private
keyset example-keyset.json start ksk-roll
keyset example-keyset.json propagation1-complete ksk-roll 1
keyset example-keyset.json cache-expired1 ksk-roll
keyset example-keyset.json propagation2-complete ksk-roll 1
keyset example-keyset.json cache-expired2 ksk-roll
keyset example-keyset.json done ksk-roll
keyset example-keyset.json actions ksk-roll
keyset example-keyset.json status
"
);
exit(1);
}
let filename = &args[1];
let command = &args[2];
if command == "init" {
do_init(filename, &args[3..]);
return;
}
if command == "addkey" {
do_addkey(filename, &args[3..]);
return;
}
if command == "deletekey" {
do_deletekey(filename, &args[3..]);
return;
}
if command == "start" {
do_start(filename, &args[3..]);
return;
}
if command == "propagation1-complete" {
do_propagation1(filename, &args[3..]);
return;
}
if command == "cache-expired1" {
do_cache_expired1(filename, &args[3..]);
return;
}
if command == "propagation2-complete" {
do_propagation2(filename, &args[3..]);
return;
}
if command == "cache-expired2" {
do_cache_expired2(filename, &args[3..]);
return;
}
if command == "done" {
do_done(filename, &args[3..]);
return;
}
if command == "actions" {
do_actions(filename, &args[3..]);
return;
}
if command == "status" {
do_status(filename, &args[3..]);
return;
}
eprintln!(
"Unknown command '{command}'. Valid commands are: init, addkey, deletekey, start, propagation1-complete, cache-expired1, propagation2-complete, cache-expired2, done, actions, and status"
);
exit(1);
}
fn do_init(filename: &str, args: &[String]) {
if args.len() != 1 {
eprintln!("Usage: ... init <domain>");
exit(1);
}
let domainname = &args[0];
let ks = KeySet::new(Name::from_str(domainname).unwrap());
save_keyset(filename, &ks);
}
fn do_addkey(filename: &str, args: &[String]) {
if args.len() < 2 || args.len() > 3 {
eprintln!("Usage: ... addkey <key-type> <pubref> [<privref>]");
exit(1);
}
let keytype = &args[0];
let pubref = args[1].clone();
let privref = if args.len() == 3 {
Some(args[2].clone())
} else {
None
};
let mut ks = load_keyset(filename);
if keytype == "ksk" {
ks.add_key_ksk(
pubref,
privref,
SecurityAlgorithm::ECDSAP256SHA256,
0,
UnixTime::now(),
Available::Available,
)
.unwrap();
} else if keytype == "zsk" {
ks.add_key_zsk(
pubref,
privref,
SecurityAlgorithm::ECDSAP256SHA256,
0,
UnixTime::now(),
Available::Available,
)
.unwrap();
} else if keytype == "csk" {
ks.add_key_csk(
pubref,
privref,
SecurityAlgorithm::ECDSAP256SHA256,
0,
UnixTime::now(),
Available::Available,
)
.unwrap();
} else {
eprintln!("Unknown key type '{keytype}'");
exit(1);
}
save_keyset(filename, &ks);
}
fn do_deletekey(filename: &str, args: &[String]) {
if args.len() != 1 {
eprintln!("Usage: ... deletekey <pubref>");
exit(1);
}
let pubref = &args[0];
let mut ks = load_keyset(filename);
if let Err(e) = ks.delete_key(pubref) {
eprintln!("Unable to delete key {pubref}: {e}");
exit(1);
}
save_keyset(filename, &ks);
}
fn do_start(filename: &str, args: &[String]) {
if args.len() != 1 {
eprintln!("Usage: ... start <roll-type>");
exit(1);
}
let rolltype = &args[0];
let rolltype = str_to_rolltype(rolltype);
let mut ks = load_keyset(filename);
// Find old and new keys. First find the keys that have the right type for
// a roll type, and map to a keystate and a pubref. Filter out the keys
// are already old, we don't take them into account for a new key roll.
// Then split into two group: complete new keys (which are not
// signer, present, or at_parent) and old keys that do have one of
// those functions.
let (old, new): (Vec<_>, Vec<_>) = ks
.keys()
.iter()
.filter_map(|(pr, k)| match rolltype {
RollType::KskRoll | RollType::KskDoubleDsRoll => {
if let KeyType::Ksk(keystate) = k.keytype() {
Some((keystate.clone(), pr))
} else {
None
}
}
RollType::ZskRoll | RollType::ZskDoubleSignatureRoll => {
if let KeyType::Zsk(keystate) = k.keytype() {
Some((keystate.clone(), pr))
} else {
None
}
}
RollType::CskRoll | RollType::AlgorithmRoll => {
match k.keytype() {
KeyType::Ksk(keystate)
| KeyType::Zsk(keystate)
| KeyType::Csk(keystate, _) => {
Some((keystate.clone(), pr))
}
KeyType::Include(_) => None,
}
}
})
.filter(|(keystate, _)| !keystate.old())
.map(|(keystate, pubref)| (keystate, pubref.to_string()))
.partition_map(|(keystate, pubref)| {
if keystate.signer() || keystate.present() || keystate.at_parent()
{
Either::Left(pubref)
} else {
Either::Right(pubref)
}
});
let old_str: Vec<&str> = old.iter().map(|s| s.as_ref()).collect();
let new_str: Vec<&str> = new.iter().map(|s| s.as_ref()).collect();
let actions = ks.start_roll(rolltype, &old_str, &new_str);
report_actions(actions, &ks);
save_keyset(filename, &ks);
}
fn do_propagation1(filename: &str, args: &[String]) {
if args.len() != 2 {
eprintln!("Usage: ... propagation1-complete <roll-type> <ttl>");
exit(1);
}
let rolltype = &args[0];
let ttl = &args[1];
let rolltype = str_to_rolltype(rolltype);
let ttl = ttl.parse().unwrap();
let mut ks = load_keyset(filename);
let actions = ks.propagation1_complete(rolltype, ttl);
report_actions(actions, &ks);
save_keyset(filename, &ks);
}
fn do_cache_expired1(filename: &str, args: &[String]) {
if args.len() != 1 {
eprintln!("Usage: ... cache-expired1 <roll-type>");
exit(1);
}
let rolltype = &args[0];
let rolltype = str_to_rolltype(rolltype);
let mut ks = load_keyset(filename);
let actions = ks.cache_expired1(rolltype);
report_actions(actions, &ks);
save_keyset(filename, &ks);
}
fn do_propagation2(filename: &str, args: &[String]) {
if args.len() != 2 {
eprintln!("Usage: ... propagation2-complete <roll-type> <ttl>");
exit(1);
}
let rolltype = &args[0];
let ttl = &args[1];
let rolltype = str_to_rolltype(rolltype);
let ttl = ttl.parse().unwrap();
let mut ks = load_keyset(filename);
let actions = ks.propagation2_complete(rolltype, ttl);
report_actions(actions, &ks);
save_keyset(filename, &ks);
}
fn do_cache_expired2(filename: &str, args: &[String]) {
if args.len() != 1 {
eprintln!("Usage: ... cache-expired2 <roll-type>");
exit(1);
}
let rolltype = &args[0];
let rolltype = str_to_rolltype(rolltype);
let mut ks = load_keyset(filename);
let actions = ks.cache_expired2(rolltype);
report_actions(actions, &ks);
save_keyset(filename, &ks);
}
fn do_done(filename: &str, args: &[String]) {
if args.len() != 1 {
eprintln!("Usage: ... done <roll-type>");
exit(1);
}
let rolltype = &args[0];
let rolltype = str_to_rolltype(rolltype);
let mut ks = load_keyset(filename);
let actions = ks.roll_done(rolltype);
report_actions(actions, &ks);
save_keyset(filename, &ks);
}
fn do_actions(filename: &str, args: &[String]) {
if args.len() != 1 {
eprintln!("Usage: ... actions <roll-type>");
exit(1);
}
let rolltype = &args[0];
let rolltype = str_to_rolltype(rolltype);
let ks = load_keyset(filename);
let actions = ks.actions(rolltype);
report_actions(Ok(actions), &ks);
}
fn do_status(filename: &str, args: &[String]) {
if !args.is_empty() {
eprintln!("Usage: ... status");
exit(1);
}
let ks = load_keyset(filename);
println!("Keys:");
let mut keys: Vec<_> = ks.keys().iter().collect();
keys.sort_by(|(pubref1, key1), (pubref2, key2)| {
(key1.timestamps().creation(), pubref1)
.cmp(&(key2.timestamps().creation(), pubref2))
});
for (pubref, key) in keys {
match key.keytype() {
KeyType::Ksk(keystate)
| KeyType::Zsk(keystate)
| KeyType::Include(keystate) => {
println!(
"\t{} {}",
pubref,
key.privref().unwrap_or_default(),
);
println!("\t\tState: {keystate}");
}
KeyType::Csk(keystate_ksk, keystate_zsk) => {
println!(
"\t{} {}",
pubref,
key.privref().unwrap_or_default(),
);
println!("\t\tKSK role state: {keystate_ksk}");
println!("\t\tZSK role state: {keystate_zsk}");
}
}
let ts = key.timestamps();
println!(
"\t\tCreated: {}",
ts.creation()
.map_or("<empty>".to_string(), |x| x.to_string()),
);
println!(
"\t\tPublished: {}",
ts.published()
.map_or("<empty>".to_string(), |x| x.to_string())
);
println!(
"\t\tVisible: {}",
ts.visible()
.map_or("<empty>".to_string(), |x| x.to_string()),
);
println!(
"\t\tDS visible: {}",
ts.ds_visible()
.map_or("<empty>".to_string(), |x| x.to_string())
);
println!(
"\t\tRRSIG visible: {}",
ts.rrsig_visible()
.map_or("<empty>".to_string(), |x| x.to_string()),
);
println!(
"\t\tWithdrawn: {}",
ts.withdrawn()
.map_or("<empty>".to_string(), |x| x.to_string())
);
}
let rolls = ks.rollstates();
let active_rolls = rolls.keys();
if active_rolls.len() == 0 {
println!("No rolls in progress");
} else {
println!("Rolls:");
for roll in active_rolls {
println!("\t{roll:?}: {:?}", rolls.get(roll).unwrap());
}
}
}
fn save_keyset(filename: &str, ks: &KeySet) {
let json = serde_json::to_string(&ks).unwrap();
let mut file = File::create(filename).unwrap();
write!(file, "{json}").unwrap();
}
fn load_keyset(filename: &str) -> KeySet {
let file = File::open(filename).unwrap();
let ks: KeySet = serde_json::from_reader(file).unwrap();
ks
}
fn str_to_rolltype(rolltype: &str) -> RollType {
match rolltype.parse() {
Ok(r) => r,
Err(e) => panic!("Error parsing '{rolltype}': {e}"),
}
}
fn report_actions(actions: Result<Vec<Action>, Error>, ks: &KeySet) {
let actions = match actions {
Err(e) => {
eprintln!("Error: {e}");
exit(1);
}
Ok(a) => a,
};
if actions.is_empty() {
println!("No actions to perform");
return;
}
println!("Actions to perform:");
for a in actions {
match a {
Action::UpdateDnskeyRrset => {
println!("\tUpdate the DNSKEY RRset");
let keys = ks.keys();
println!("\t\tKeys in the DNSKEY RRset:");
for (pubref, key) in keys {
let status = match key.keytype() {
KeyType::Ksk(keystate)
| KeyType::Zsk(keystate)
| KeyType::Csk(keystate, _)
| KeyType::Include(keystate) => keystate,
};
if status.present() {
println!("\t\t\t{pubref}");
}
}
println!("\t\tKeys signing the DNSKEY RRset:");
for (pubref, key) in keys {
match key.keytype() {
KeyType::Ksk(keystate)
| KeyType::Csk(keystate, _) => {
if keystate.signer() {
println!("\t\t\t{pubref}");
}
}
KeyType::Zsk(_) | KeyType::Include(_) => (),
}
}
}
Action::UpdateRrsig => {
println!("\tSign the zone with the following keys:");
let keys = ks.keys();
for (pubref, key) in keys {
match key.keytype() {
KeyType::Zsk(keystate)
| KeyType::Csk(_, keystate) => {
if keystate.signer() {
println!("\t\t{pubref}");
}
}
KeyType::Ksk(_) | KeyType::Include(_) => (),
}
}
}
Action::UpdateDsRrset => {
println!(
"\tUpdate the DS records at the parent to contain just the following keys:"
);
let keys = ks.keys();
for (pubref, key) in keys {
let status = match key.keytype() {
KeyType::Ksk(keystate)
| KeyType::Zsk(keystate)
| KeyType::Csk(keystate, _)
| KeyType::Include(keystate) => keystate,
};
if status.at_parent() {
println!("\t\t{pubref}");
}
}
}
Action::CreateCdsRrset => {
println!(
"\tCreate CDS and CDNSKEY RRsets with the following keys:"
);
let keys = ks.keys();
for (pubref, key) in keys {
let status = match key.keytype() {
KeyType::Ksk(keystate)
| KeyType::Zsk(keystate)
| KeyType::Csk(keystate, _)
| KeyType::Include(keystate) => keystate,
};
if status.at_parent() {
println!("\t\t{pubref}");
}
}
}
Action::RemoveCdsRrset => {
println!("\tRemove CDS and CDNSKEY RRsets")
}
Action::ReportDnskeyPropagated => {
println!("\tReport that the DNSKEY RRset has propagated")
}
Action::WaitDnskeyPropagated => {
println!("\tWait until the DNSKEY RRset has propagated")
}
Action::ReportRrsigPropagated => {
println!("\tReport that the RRSIG records have propagated")
}
Action::WaitRrsigPropagated => {
println!("\tWait until the RRSIG records have propagated")
}
Action::ReportDsPropagated => println!(
"\tReport that the DS RRset has propagated at the parent"
),
Action::WaitDsPropagated => {
println!("\tWait until DS RRset has propagated at the parent")
}
}
}
}