use domain::base::iana::SecurityAlgorithm;
use domain::base::Name;
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 {
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);
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")
}
}
}
}