diff --git a/src/signer/full.rs b/src/signer/full.rs new file mode 100644 index 00000000..e66591e5 --- /dev/null +++ b/src/signer/full.rs @@ -0,0 +1,551 @@ +//! Full (non-incremental) signing. + +use std::{ + cmp::Ordering, + collections::HashSet, + env::{self, VarError}, + sync::{Arc, RwLock}, + time::Instant, +}; + +use bytes::Bytes; +use cascade_zonedata::{OldRecord, RegularRecord, SignedZoneBuilder}; +use domain::{ + base::{CanonicalOrd, Record, Serial, name::FlattenInto}, + dnssec::sign::{ + denial::{ + config::DenialConfig, + nsec3::{GenerateNsec3Config, Nsec3ParamTtlMode, Nsec3Records, generate_nsec3s}, + }, + error::SigningError, + keys::keyset::KeyType, + records::RecordsIter, + signatures::rrsigs::GenerateRrsigConfig, + }, + rdata::{Nsec3param, dnssec::Timestamp}, + zonefile::inplace::{Entry, Zonefile}, +}; +use domain::{ + dnssec::sign::{ + SigningConfig, denial::nsec::generate_nsecs, keys::keyset::UnixTime, + signatures::rrsigs::sign_sorted_zone_records, + }, + new::{ + base::{RType, Serial as NewBaseSerial}, + rdata::RecordData, + }, + rdata::ZoneRecordData, +}; +use jiff::{Timestamp as JiffTimestamp, Zoned, tz::TimeZone}; +use rayon::{ + iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelExtend, ParallelIterator}, + slice::ParallelSliceMut, +}; +use tracing::{debug, info}; + +use crate::{ + center::Center, + manager::record_zone_event, + policy::{PolicyVersion, SignerDenialPolicy, SignerSerialPolicy}, + signer::{ + SigningTrigger, + incremental::LocalState, + keys::ZoneSigningKeys, + status::{SigningStatusPerZone, ZoneSigningStatus}, + }, + units::{ + key_manager::mk_dnst_keyset_state_file_path, + zone_signer::{KeySetState, MinTimestamp, SignerError}, + }, + zone::{HistoricalEvent, Zone}, +}; + +pub fn sign_zone( + center: &Arc
, + zone: &Arc, + builder: &mut SignedZoneBuilder, + trigger: SigningTrigger, + status: Arc>, +) -> Result<(), SignerError> { + let zone_name = &zone.name; + + info!("[ZS]: Starting signing operation for zone '{zone_name}'"); + let start = Instant::now(); + + let mut local_state = LocalState::new(zone)?; + + let policy = { + // Use a block to make sure that the lock is clearly dropped. + let zone_state = zone.read(); + + zone_state.policy.clone().unwrap() + }; + let previous_serial = local_state.previous_serial; + + // + // Lookup the zone to sign. + // + let mut writer = builder.replace().unwrap(); + let mut new_records = Vec::new(); + let loaded = writer + .next_loaded() + .or(writer.curr_loaded()) + .expect("a non-empty loaded instance must exist"); + let loaded_serial = loaded.soa().rdata.serial; + + let serial: Serial = match policy.signer.serial_policy { + SignerSerialPolicy::Keep => { + let loaded_serial = Serial::from(Into::::into(loaded_serial)); + if let Some(previous_serial) = previous_serial + && loaded_serial <= previous_serial + { + return Err(SignerError::KeepSerialPolicyViolated); + } + + loaded_serial + } + SignerSerialPolicy::Counter => { + // Always increment the serial number, ignore the serial + // number in the unsigned zone. + let previous_serial = previous_serial.unwrap_or(Serial::from(0)); + previous_serial.add(1) + } + SignerSerialPolicy::UnixTime => { + let mut serial = Serial::now(); + if let Some(previous_serial) = previous_serial + && serial <= previous_serial + { + serial = previous_serial.add(1); + } + + serial + } + SignerSerialPolicy::DateCounter => { + let ts = JiffTimestamp::now(); + let zone = Zoned::new(ts, TimeZone::UTC); + let serial = + ((zone.year() as u32 * 100 + zone.month() as u32) * 100 + zone.day() as u32) * 100; + let mut serial: Serial = serial.into(); + + if let Some(previous_serial) = previous_serial + && serial <= previous_serial + { + serial = previous_serial.add(1); + } + + serial + } + }; + local_state.previous_serial = Some(serial); + let serial = NewBaseSerial::from(serial.into_int()); + let new_soa = { + let mut soa = loaded.soa().clone(); + soa.rdata.serial = serial; + soa + }; + new_records.push(new_soa.clone().into()); + + info!( + "[ZS]: Serials for zone '{zone_name}': last signed={previous_serial:?}, current={loaded_serial}, serial policy={}, new={serial}", + policy.signer.serial_policy + ); + + // + // Record the start of signing for this zone. + // + { + status + .write() + .unwrap() + .status + .start(loaded_serial) + .map_err(|_| SignerError::InternalError("Invalid status".to_string()))?; + } + + // + // Create a signing configuration. + // + let signing_config = signing_config(&policy)?; + let rrsig_cfg = GenerateRrsigConfig::new(signing_config.inception, signing_config.expiration); + + // + // Convert zone records into a form we can sign. + // + status.write().unwrap().current_action = "Collecting records to sign".to_string(); + debug!("[ZS]: Collecting records to sign for zone '{zone_name}'."); + let walk_start = Instant::now(); + let mut records = loaded + .unsigned_records() + .filter(|r| r.rname != new_soa.rname || r.rtype != new_soa.rtype) + .cloned() + .map(OldRecord::from) + .collect::>(); + records.push(new_soa.clone().into()); + let walk_time = walk_start.elapsed(); + let unsigned_rr_count = records.len(); + + { + let mut v = status.write().unwrap(); + let v2 = &mut v.status; + if let ZoneSigningStatus::InProgress(s) = v2 { + s.unsigned_rr_count = Some(unsigned_rr_count); + s.walk_time = Some(walk_time); + } + } + + debug!("Reading dnst keyset DNSKEY RRs and RRSIG RRs"); + status.write().unwrap().current_action = "Fetching apex RRs from the key manager".to_string(); + // Read the DNSKEY RRs and DNSKEY RRSIG RR from the keyset state. + let state_path = mk_dnst_keyset_state_file_path(¢er.config.keys_dir, &zone.name); + let state = std::fs::read_to_string(&state_path) + .map_err(|_| SignerError::CannotReadStateFile(state_path.into_string()))?; + let state: KeySetState = serde_json::from_str(&state).unwrap(); + + local_state.apex_remove = state.apex_remove.clone(); + let mut apex_extra = state.apex_extra.clone(); + apex_extra.sort(); + local_state.apex_extra = apex_extra; + + for rr in &state.apex_extra { + let mut zonefile = Zonefile::new(); + zonefile.extend_from_slice(rr.as_bytes()); + zonefile.extend_from_slice(b"\n"); + if let Ok(Some(Entry::Record(rec))) = zonefile.next_entry() { + let record: OldRecord = rec.flatten_into(); + new_records.push(record.clone().into()); + records.push(record); + } + } + + debug!("Loading dnst keyset signing keys"); + // Load the signing keys indicated by the keyset state. + let signing_keys = ZoneSigningKeys::load(center, zone, &state, &status)?; + + // Save the current zone signing keys and clear key_roll + let mut key_tags = HashSet::new(); + for v in state.keyset.keys().values() { + let signer = match v.keytype() { + KeyType::Ksk(_) => false, + KeyType::Zsk(key_state) => key_state.signer(), + KeyType::Csk(_, key_state) => key_state.signer(), + KeyType::Include(_) => false, + }; + + if !signer { + continue; + } + + key_tags.insert(v.key_tag()); + } + local_state.key_tags = key_tags; + local_state.key_roll = None; + + // + // Sort them into DNSSEC order ready for NSEC(3) generation. + // + debug!("[ZS]: Sorting collected records for zone '{zone_name}'."); + status.write().unwrap().current_action = "Sorting records".to_string(); + let sort_start = Instant::now(); + // Note: This may briefly use lots of CPU and many CPU cores. + records.par_sort_by(CanonicalOrd::canonical_cmp); + let sort_time = sort_start.elapsed(); + let unsigned_rr_count = records.len(); + + { + let mut v = status.write().unwrap(); + let v2 = &mut v.status; + if let ZoneSigningStatus::InProgress(s) = v2 { + s.sort_time = Some(sort_time); + } + } + + // + // Generate NSEC(3) RRs. + // + debug!("[ZS]: Generating denial records for zone '{zone_name}'."); + status.write().unwrap().current_action = "Generating denial records".to_string(); + let denial_start = Instant::now(); + match &signing_config.denial { + DenialConfig::AlreadyPresent => {} + + DenialConfig::Nsec(cfg) => { + let nsecs = generate_nsecs(&zone.name, RecordsIter::new_from_owned(&records), cfg) + .map_err(|err: SigningError| { + SignerError::SigningError(format!("Failed to generate denial RRs: {err}")) + })?; + + new_records.par_extend( + nsecs + .par_iter() + .map(|r| OldRecord::from_record(r.clone()).into()), + ); + records.par_extend(nsecs.into_par_iter().map(Record::from_record)); + } + + DenialConfig::Nsec3(cfg) => { + // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash + // order." We store the NSEC3s as we create them and sort them + // afterwards. + let Nsec3Records { nsec3s, nsec3param } = + generate_nsec3s(&zone.name, RecordsIter::new_from_owned(&records), cfg).map_err( + |err: SigningError| { + SignerError::SigningError(format!("Failed to generate denial RRs: {err}")) + }, + )?; + + // Add the generated NSEC3 records. + new_records.par_extend( + nsec3s + .par_iter() + .map(|r| OldRecord::from_record(r.clone()).into()), + ); + new_records.push(OldRecord::from_record(nsec3param.clone()).into()); + records.par_extend(nsec3s.into_par_iter().map(Record::from_record)); + records.push(Record::from_record(nsec3param)); + } + } + // Use a stable sort; the stable sort algorithm detects runs of sorted + // elements ('records' contains two concatenated pre-sorted runs) and + // can efficiently sort around them. + records.par_sort_by(CanonicalOrd::canonical_cmp); + + let unsigned_records = records; + let denial_time = denial_start.elapsed(); + let denial_rr_count = unsigned_records.len() - unsigned_rr_count; + + { + let mut v = status.write().unwrap(); + let v2 = &mut v.status; + if let ZoneSigningStatus::InProgress(s) = v2 { + s.denial_rr_count = Some(denial_rr_count); + s.denial_time = Some(denial_time); + } + } + + // + // Generate RRSIG RRs concurrently. + // + // Use N concurrent Rayon scoped threads to do blocking RRSIG + // generation without interfering with Tokio task scheduling, and an + // async task which receives generated RRSIGs via a Tokio + // mpsc::channel and accumulates them into the signed zone. + // + debug!("[ZS]: Generating RRSIG records."); + status.write().unwrap().current_action = "Generating signature records".to_string(); + + // TODO: Configure Rayon's thread pool to set the number of threads. By + // default, it relies on 'std::thread::available_parallelism()'. + let parallelism = rayon::current_num_threads(); + + { + let mut v = status.write().unwrap(); + let v2 = &mut v.status; + if let ZoneSigningStatus::InProgress(s) = v2 { + s.threads_used = Some(parallelism); + } + } + + let generation_start = Instant::now(); + + // Get the keys to sign with. Domain's 'sign_sorted_zone_records()' + // needs a slice of references, so we need to build that here. + let keys = signing_keys.list.iter().collect::>(); + + // TODO: This generation code is incorrect; 'sign_sorted_zone_records' + // looks for zone cuts, but zone cuts may need to be detected _across_ + // the segments we split the records into. Zone cut detection needs to + // be re-implemented here with parallel execution in mind. This also + // applies to NSEC(3) generation, but it is currently single-threaded. + + // Disable parallel signing for now. This may also split RRsets. + let signatures = if false { + // Split the records into segments. + let segments = rayon::iter::split(0..unsigned_records.len(), |range| { + // Always sign at least 1024 records at a time. + if range.len() < 1024 { + return (range, None); + } + + let midpoint = range.start + range.len() / 2; + let left = range.start..midpoint; + let right = midpoint..range.end; + (left, Some(right)) + }); + + // Generate signatures from each segment. + let signatures = segments.map(|range| { + sign_sorted_zone_records( + &zone.name, + RecordsIter::new_from_owned(&unsigned_records[range]), + &keys, + &rrsig_cfg, + ) + }); + + // Convert the signatures into new-base types and collect them together. + // If errors occur, one error is arbitrarily chosen and returned. + signatures + .try_fold(Vec::new, |mut a, b| { + a.extend(b?.into_iter().map(|r| OldRecord::from_record(r).into())); + Ok::<_, SigningError>(a) + }) + .try_reduce(Vec::new, |mut a, mut b| { + a.append(&mut b); + Ok(a) + }) + .map_err(|err| SignerError::SigningError(err.to_string()))? + } else { + let signatures = sign_sorted_zone_records( + &zone.name, + RecordsIter::new_from_owned(&unsigned_records), + &keys, + &rrsig_cfg, + ) + .map_err(|err| SignerError::SigningError(err.to_string()))?; + let signatures: Vec = signatures + .into_iter() + .map(|s| { + let r = Record::new( + s.owner().clone(), + s.class(), + s.ttl(), + ZoneRecordData::Rrsig(s.data().clone()), + ); + r.into() + }) + .collect(); + signatures + }; + + let total_signatures = signatures.len(); + + new_records.extend(signatures); + new_records.par_sort(); + writer.set_records(new_records).unwrap(); + + let generation_time = generation_start.elapsed(); + + let generation_rate = total_signatures as f64 / generation_time.as_secs_f64().min(0.001); + + writer.set_soa(new_soa.clone()).unwrap(); + writer.apply().unwrap(); + + debug!("SIGNER: Determining min expiration time"); + let reader = builder.next_signed().unwrap(); + let min_expiration = Arc::new(MinTimestamp::new()); + let saved_min_expiration = min_expiration.clone(); + for record in reader.generated_records() { + let RecordData::Rrsig(sig) = record.rdata.get() else { + continue; + }; + + // Ignore RRSIG records for DNSKEY, CDS, and CDNSKEY records; these + // are generated by the key manager, using KSKs. + if sig.rtype == RType::DNSKEY + || sig.rtype == RType::from(59) + || sig.rtype == RType::from(60) + { + continue; + } + + min_expiration.add(u32::from(sig.expiration).into()); + } + local_state.next_min_expiration = saved_min_expiration.get(); + + let total_time = start.elapsed(); + + { + let mut v = status.write().unwrap(); + let v2 = &mut v.status; + if let ZoneSigningStatus::InProgress(s) = v2 { + s.rrsig_count = Some(total_signatures); + s.rrsig_reused_count = Some(0); // Not implemented yet + s.rrsig_time = Some(generation_time); + s.total_time = Some(total_time); + } + v.status.finish(true); + } + + // Log signing statistics. + info!( + "Signing statistics for {zone_name} serial: {serial}:\n\ + Collected {unsigned_rr_count} records in {:.1}s, sorted in {:.1}s\n\ + Generated {denial_rr_count} NSEC(3) records in {:.1}s\n\ + Generated {total_signatures} signatures in {:.1}s ({generation_rate:.0}sig/s) + Took {:.1}s in total, using {parallelism} threads", + walk_time.as_secs_f64(), + sort_time.as_secs_f64(), + denial_time.as_secs_f64(), + generation_time.as_secs_f64(), + total_time.as_secs_f64() + ); + + record_zone_event( + center, + zone, + HistoricalEvent::SigningSucceeded { + trigger: trigger.into(), + }, + Some(Serial(serial.into())), + ); + + local_state.last_signature_refresh = UnixTime::now(); + local_state.save(center, zone); + + Ok(()) +} + +//----------- signing_config() ------------------------------------------------- + +fn signing_config( + policy: &PolicyVersion, +) -> Result, SignerError> { + let denial = match &policy.signer.denial { + SignerDenialPolicy::NSec => DenialConfig::Nsec(Default::default()), + SignerDenialPolicy::NSec3 { opt_out } => { + let first = parse_nsec3_config(*opt_out); + DenialConfig::Nsec3(first) + } + }; + + let now = match env::var("CASCADE_FAKETIME") { + Ok(val) => val + .parse::() + .map_err(|e| SignerError::InternalError(format!("cannot parse {e} as u32")))?, + Err(VarError::NotPresent) => Timestamp::now().into_int(), + Err(e) => return Err(SignerError::InternalError(e.to_string())), + }; + let inception = now.wrapping_sub(policy.signer.sig_inception_offset); + let expiration = now.wrapping_add(policy.signer.sig_validity_time); + Ok(SigningConfig::new( + denial, + inception.into(), + expiration.into(), + )) +} + +fn parse_nsec3_config(opt_out: bool) -> GenerateNsec3Config { + let mut params = Nsec3param::default(); + if opt_out { + params.set_opt_out_flag() + } + + // TODO: support other ttl_modes? Seems missing from the config right now + let ttl_mode = Nsec3ParamTtlMode::Soa; + GenerateNsec3Config::new(params).with_ttl_mode(ttl_mode) +} + +//------------ MultiThreadedSorter ------------------------------------------- + +/// A parallelized sort implementation for signing. +struct MultiThreadedSorter; + +impl domain::dnssec::sign::records::Sorter for MultiThreadedSorter { + fn sort_by(records: &mut Vec>, compare: F) + where + F: Fn(&Record, &Record) -> Ordering + Sync, + Record: CanonicalOrd + Send, + { + records.par_sort_by(compare); + } +} diff --git a/src/signer/incremental.rs b/src/signer/incremental.rs index a8942236..1ccbb6e9 100644 --- a/src/signer/incremental.rs +++ b/src/signer/incremental.rs @@ -27,7 +27,6 @@ use domain::dnssec::sign::denial::nsec::{GenerateNsecConfig, generate_nsecs}; use domain::dnssec::sign::denial::nsec3::{ GenerateNsec3Config, Nsec3ParamTtlMode, generate_nsec3s, }; -use domain::dnssec::sign::keys::SigningKey; use domain::dnssec::sign::keys::keyset::{KeyType, UnixTime}; use domain::dnssec::sign::records::{DefaultSorter, RecordsIter, Rrset}; use domain::dnssec::sign::signatures::rrsigs::sign_rrset; @@ -54,16 +53,15 @@ use crate::center::Center; use crate::manager::record_zone_event; use crate::policy::{PolicyVersion, SignerDenialPolicy, SignerSerialPolicy}; use crate::signer::SigningTrigger; +use crate::signer::keys::ZoneSigningKeys; use crate::signer::status::SigningStatusPerZone; use crate::units::key_manager::mk_dnst_keyset_state_file_path; use crate::units::zone_signer::{ - KeyPair, KeySetState, MinTimestamp, PassThroughMode, SignerError, ZoneSigner, faketime_or_now, - load_keys, + KeySetState, MinTimestamp, PassThroughMode, SignerError, faketime_or_now, }; use crate::zone::{HistoricalEvent, Zone}; pub fn sign_incrementally( - zone_signer: &ZoneSigner, patch: SignedZonePatcher, zone: &Arc, center: &Arc
, @@ -137,14 +135,7 @@ pub fn sign_incrementally( return Err(SignerError::NothingToDo); } - let mut iss = IncrementalSigningState::new( - origin.clone(), - &policy, - zone_signer, - center, - &ws.keyset_state, - status, - )?; + let mut iss = IncrementalSigningState::new(zone, &policy, center, &ws.keyset_state, status)?; let start = Instant::now(); let patch_curr = ws.patch.curr(); @@ -1499,7 +1490,7 @@ struct IncrementalSigningState<'zd> { modified_nsecs: HashSet>, /// Signing keys. - keys: Vec>, + keys: ZoneSigningKeys, /// Inception time to use for signatures. inception: Timestamp, @@ -1513,14 +1504,13 @@ struct IncrementalSigningState<'zd> { impl<'a> IncrementalSigningState<'a> { pub fn new( - origin: Name, + zone: &Zone, policy: &PolicyVersion, - zone_signer: &ZoneSigner, center: &Arc
, keyset_state: &KeySetState, status: Arc>, ) -> Result { - let keys = load_keys(zone_signer, center, origin.clone(), keyset_state, status)?; + let keys = ZoneSigningKeys::load(center, zone, keyset_state, &status)?; let now = faketime_or_now(); let now_u32 = Into::::into(now.clone()).as_secs() as u32; @@ -1540,7 +1530,7 @@ impl<'a> IncrementalSigningState<'a> { } } Ok(Self { - origin, + origin: zone.name.clone(), old_apex: HashMap::new(), old_apex_saved: HashMap::new(), new_apex: HashMap::new(), @@ -2678,7 +2668,7 @@ impl LocalState { fn sign_records( origin: &Name, records: &[Zrd], - keys: &[SigningKey], + keys: &ZoneSigningKeys, inception: Timestamp, expiration: Timestamp, new_sigs: &mut Vec>, @@ -2696,7 +2686,7 @@ fn sign_records( let rrset = Rrset::new_from_refs(&records) .map_err(|e| SignerError::SigningError(format!("Rrset::new failed: {e}")))?; let mut rrsig_records = vec![]; - for key in keys { + for key in &keys.list { let rrsig = sign_rrset(key, &rrset, inception, expiration) .map_err(|e| SignerError::SigningError(format!("signing failed: {e}")))?; let record = Record::new( diff --git a/src/signer/keys.rs b/src/signer/keys.rs new file mode 100644 index 00000000..f83307ce --- /dev/null +++ b/src/signer/keys.rs @@ -0,0 +1,654 @@ +//! Handling signing keys. + +use core::fmt; +use std::{ + sync::{Arc, RwLock}, + time::Duration, +}; + +use bytes::Bytes; +use camino::Utf8Path; +use domain::{ + base::{Name, Record, iana::SecurityAlgorithm}, + crypto::sign::{BindFormatError, SecretKeyBytes, SignError, SignRaw, Signature}, + dnssec::{ + common::{ParseDnskeyTextError, parse_from_bind}, + sign::keys::{SigningKey, keyset::KeyType}, + }, + rdata::Dnskey, +}; +use domain_kmip::{ + ConnectionSettings, KeyUrl, + dep::kmip::client::pool::{ConnectionManager, KmipConnError}, +}; +use tracing::{debug, error, warn}; +use url::Url; + +use crate::{ + center::Center, + signer::status::SigningStatusPerZone, + units::{ + http_server::KmipServerState, + key_manager::{KmipClientCredentialsFile, KmipServerCredentialsFileMode}, + zone_signer::KeySetState, + }, + zone::Zone, +}; + +//----------- ZoneSigningKeys -------------------------------------------------- + +/// A set of keys for signing a zone. +/// +/// These are zone signing keys (ZSKs) and combined signing keys (CSKs) that the +/// key manager indicates should be used for signing a zone. +#[derive(Debug)] +pub struct ZoneSigningKeys { + /// The underlying list of keys. + /// + /// This list should be non-empty. + pub list: Vec>, +} + +impl ZoneSigningKeys { + /// Load the keys that should be used to sign a zone. + /// + /// ## Panics + /// + /// Panics if `keyset_state` is malformed (e.g. contains invalid URLs). + #[tracing::instrument( + level = "debug", + skip_all, + fields(zone = %zone.name), + )] + pub fn load( + center: &Center, + zone: &Zone, + keyset_state: &KeySetState, + status: &RwLock, + ) -> Result> { + status.write().unwrap().current_action = "Loading signing keys".to_string(); + + let mut list = Vec::new(); + + for (pub_key_name, key_info) in keyset_state.keyset.keys() { + let (KeyType::Zsk(key_state) | KeyType::Csk(_, key_state)) = key_info.keytype() else { + debug!("Ignoring key {pub_key_name:?}: Not a ZSK or CSK"); + continue; + }; + + if !key_state.signer() { + debug!("Ignoring key {pub_key_name:?}: Not enabled by keyset"); + continue; + } + + let Some(priv_key_name) = key_info.privref() else { + debug!("Ignoring key {pub_key_name:?}: Don't have a private key"); + continue; + }; + + let priv_url = Url::parse(priv_key_name).expect("valid URL expected"); + let pub_url = Url::parse(pub_key_name).expect("valid URL expected"); + + if priv_url.scheme() != pub_url.scheme() { + return Err(Box::new(LoadError::MultipleSchemesInKey { + pub_url, + priv_url, + })); + } + + let keypair = match priv_url.scheme() { + "file" => KeyPair::load_from_disk( + zone, + priv_url.path().as_ref(), + pub_url.path().as_ref(), + )?, + "kmip" => { + let priv_url = KeyUrl::try_from(priv_url.clone()).map_err(|error| { + Box::new(LoadError::MalformedKmipKeyUrl { + url: priv_url.clone(), + error, + }) + })?; + let pub_url = KeyUrl::try_from(pub_url.clone()).map_err(|error| { + Box::new(LoadError::MalformedKmipKeyUrl { + url: pub_url.clone(), + error, + }) + })?; + KeyPair::load_kmip(center, priv_url, pub_url, status)? + } + _ => { + return Err(Box::new(LoadError::UnsupportedScheme { url: pub_url })); + } + }; + + let key = SigningKey::new(zone.name.clone(), keypair.dnskey().flags(), keypair); + + debug!("Successfully loaded key '{priv_url}' + '{pub_url}'"); + list.push(key); + } + + debug!("Loaded {} signing key(s).", list.len()); + + // TODO: If signing is disabled for a zone should we then allow the + // unsigned zone to propagate through the pipeline? + if list.is_empty() { + error!("No applicable signing keys were found"); + return Err(Box::new(LoadError::NoKeysFound)); + } + + Ok(Self { list }) + } +} + +//----------- KeyPair ---------------------------------------------------------- + +/// A cryptographic keypair for signing. +#[derive(Debug)] +pub enum KeyPair { + /// A keypair provided by [`domain`]. + Domain(domain::crypto::sign::KeyPair), + + /// A KMIP keypair. + Kmip(domain_kmip::sign::KeyPair), +} + +//--- Signing + +impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + match self { + KeyPair::Domain(k) => k.algorithm(), + KeyPair::Kmip(k) => k.algorithm(), + } + } + + fn dnskey(&self) -> Dnskey> { + match self { + KeyPair::Domain(k) => k.dnskey(), + KeyPair::Kmip(k) => k.dnskey(), + } + } + + fn sign_raw(&self, data: &[u8]) -> Result { + match self { + KeyPair::Domain(k) => k.sign_raw(data), + KeyPair::Kmip(k) => k.sign_raw(data), + } + } +} + +//--- Loading from disk + +impl KeyPair { + /// Load a key-pair from the disk. + pub fn load_from_disk( + zone: &Zone, + priv_key_path: &Utf8Path, + pub_key_path: &Utf8Path, + ) -> Result> { + debug!("Loading the on-disk private key '{priv_key_path}'"); + let priv_key = Self::load_priv_from_file(priv_key_path)?; + + debug!("Loading the on-disk public key '{pub_key_path}'"); + let pub_key = Self::load_pub_from_file(pub_key_path)?; + + let key_pair = domain::crypto::sign::KeyPair::from_bytes(&priv_key, pub_key.data()) + .map_err(|error| { + Box::new(LoadError::MalformedOnDiskKeyPair { + priv_key_path: priv_key_path.into(), + pub_key_path: pub_key_path.into(), + error, + }) + })?; + + if pub_key.owner() != &zone.name { + let encoded_owner = pub_key.owner(); + let zone_name = &zone.name; + warn!( + "The public key at '{pub_key_path}' \ + encodes the owner name '{encoded_owner}', \ + but this will be ignored in favor of \ + the name of the zone, '{zone_name}'" + ); + } + + Ok(Self::Domain(key_pair)) + } + + /// Load a private key from a file. + fn load_priv_from_file(path: &Utf8Path) -> Result> { + let encoded = std::fs::read_to_string(path).map_err(|error| { + Box::new(LoadError::UnreadableKeyFile { + path: path.into(), + error, + }) + })?; + + // TODO: Compared to the original ldns-signzone there is a minor + // regression here because at the time of writing the error returned + // from parsing indicates broadly the type of parsing failure but does + // note indicate the line number at which parsing failed. + let secret_key = SecretKeyBytes::parse_from_bind(&encoded).map_err(|error| { + Box::new(LoadError::MalformedPrivateKeyFile { + path: path.into(), + error, + }) + })?; + + Ok(secret_key) + } + + /// Load a public key from a file. + fn load_pub_from_file( + path: &Utf8Path, + ) -> Result, Dnskey>, Box> { + let encoded = std::fs::read_to_string(path).map_err(|error| { + Box::new(LoadError::UnreadableKeyFile { + path: path.into(), + error, + }) + })?; + + // TODO: Compared to the original ldns-signzone there is a minor + // regression here because at the time of writing the error returned + // from parsing indicates broadly the type of parsing failure but does + // note indicate the line number at which parsing failed. + let public_key = parse_from_bind(&encoded).map_err(|error| { + Box::new(LoadError::MalformedPublicKeyFile { + path: path.into(), + error, + }) + })?; + + Ok(public_key) + } +} + +//--- Loading from KMIP + +impl KeyPair { + /// Load a KMIP key-pair. + pub fn load_kmip( + center: &Center, + priv_key_url: KeyUrl, + pub_key_url: KeyUrl, + status: &RwLock, + ) -> Result> { + // TODO: Replace the connection pool if the persisted KMIP server settings + // were updated more recently than the pool was created. + + let mut kmip_servers = center.signer.kmip_servers.lock().unwrap(); + let kmip_conn_pool = match kmip_servers.entry(priv_key_url.server_id().to_string()) { + std::collections::hash_map::Entry::Occupied(e) => e.into_mut(), + std::collections::hash_map::Entry::Vacant(e) => { + status.write().unwrap().current_action = + format!("Connecting to KMIP server '{}'", priv_key_url.server_id()); + + // Try and load the KMIP server settings. + let server_state_path = center + .config + .kmip_server_state_dir + .join(priv_key_url.server_id()); + debug!("Reading KMIP server state from '{server_state_path}'"); + let f = std::fs::File::open(&server_state_path).map_err(|error| { + Box::new(LoadError::UnreadableKmipServerState { + path: server_state_path.clone().into(), + error, + }) + })?; + let kmip_server: KmipServerState = serde_json::from_reader(f).map_err(|error| { + Box::new(LoadError::MalformedKmipServerState { + path: server_state_path.clone().into(), + error, + }) + })?; + let KmipServerState { + server_id, + ip_host_or_fqdn: host, + port, + insecure, + connect_timeout, + read_timeout, + write_timeout, + max_response_bytes, + has_credentials, + .. + } = kmip_server; + + let mut username = None; + let mut password = None; + if has_credentials { + let creds_path = ¢er.config.kmip_credentials_store_path; + let creds_file = KmipClientCredentialsFile::new( + creds_path.as_std_path(), + KmipServerCredentialsFileMode::ReadOnly, + ) + .map_err(|error| { + Box::new(LoadError::KmipClientCredentials { + path: creds_path.clone(), + error, + }) + })?; + + let creds = creds_file.get(&server_id).ok_or_else(|| { + Box::new(LoadError::MissingKmipClientCredentials { + server_id: server_id.clone().into(), + path: creds_path.clone(), + }) + })?; + + username = Some(creds.username.clone()); + password = creds.password.clone(); + } + + let conn_settings = ConnectionSettings { + host, + port, + username, + password, + insecure, + client_cert: None, // TODO + server_cert: None, // TODO + ca_cert: None, // TODO + connect_timeout: Some(connect_timeout), + read_timeout: Some(read_timeout), + write_timeout: Some(write_timeout), + max_response_bytes: Some(max_response_bytes), + }; + + debug!("Connecting to KMIP server '{server_id}'"); + let pool = ConnectionManager::create_connection_pool( + server_id.clone(), + Arc::new(conn_settings.clone()), + 10, + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), + ) + .map_err(|error| { + Box::new(LoadError::KmipConnection { + server_id: server_id.into(), + error, + }) + })?; + + e.insert(pool) + } + }; + + status.write().unwrap().current_action = format!( + "Fetching keys from KMIP server '{}'", + priv_key_url.server_id() + ); + + let priv_key_url_inner = (*priv_key_url).clone(); + let pub_key_url_inner = (*pub_key_url).clone(); + + let key_pair = Self::Kmip( + domain_kmip::sign::KeyPair::from_urls( + priv_key_url, + pub_key_url, + kmip_conn_pool.clone(), + ) + .map_err(|error| { + Box::new(LoadError::MalformedKmipKeypair { + priv_key_url: priv_key_url_inner, + pub_key_url: pub_key_url_inner, + error: error.to_string(), + }) + })?, + ); + + Ok(key_pair) + } +} + +//============ Errors ========================================================== + +//----------- LoadError -------------------------------------------------------- + +/// An error loading [`ZoneSigningKeys`]. +#[derive(Debug)] +pub enum LoadError { + /// No applicable zone signing keys were found. + NoKeysFound, + + /// A key uses multiple key URI schemes. + MultipleSchemesInKey { + /// The URL of the public key. + pub_url: Url, + + /// The URL of the private key. + priv_url: Url, + }, + + /// A public/private key uses an unsupported URI scheme. + UnsupportedScheme { + /// The URL of the key. + url: Url, + }, + + /// A public/private key could not be read from a file. + UnreadableKeyFile { + /// The path to the key. + path: Box, + + /// The underlying I/O error. + error: std::io::Error, + }, + + /// An on-disk private key could not be parsed. + MalformedPrivateKeyFile { + /// The path to the key. + path: Box, + + /// The underlying error. + error: BindFormatError, + }, + + /// An on-disk public key could not be parsed. + MalformedPublicKeyFile { + /// The path to the key. + path: Box, + + /// The underlying error. + error: ParseDnskeyTextError, + }, + + /// An on-disk key-pair was malformed. + MalformedOnDiskKeyPair { + /// The path to the private key. + priv_key_path: Box, + + /// The path to the public key. + pub_key_path: Box, + + /// The underlying error. + error: domain::crypto::sign::FromBytesError, + }, + + /// A KMIP key URL was malformed. + MalformedKmipKeyUrl { + /// The URL of the key. + url: Url, + + /// The underlying error. + error: String, + }, + + /// Could not read the KMIP server state. + UnreadableKmipServerState { + /// The path to the state file. + path: Box, + + /// The underlying error. + error: std::io::Error, + }, + + /// Could not parse the KMIP server state. + MalformedKmipServerState { + /// The path to the state file. + path: Box, + + /// The underlying error. + error: serde_json::Error, + }, + + /// Could not load the KMIP client credentials file. + KmipClientCredentials { + /// The path to the credentials file. + path: Box, + + /// The underlying error. + error: String, + }, + + /// Missing credentials for a KMIP server. + MissingKmipClientCredentials { + /// The name of the KMIP server. + server_id: Box, + + /// The path to the credentials file. + path: Box, + }, + + /// Could not connect to a KMIP server. + KmipConnection { + /// The name of the KMIP server. + server_id: Box, + + /// The underlying error. + error: KmipConnError, + }, + + /// A KMIP key-pair was malformed. + MalformedKmipKeypair { + /// The URL of the private key. + priv_key_url: Url, + + /// The URL of the public key. + pub_key_url: Url, + + /// The underlying error. + error: String, + }, +} + +impl core::error::Error for LoadError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::NoKeysFound => None, + Self::MultipleSchemesInKey { .. } => None, + Self::UnsupportedScheme { .. } => None, + Self::UnreadableKeyFile { error, .. } => Some(error), + Self::MalformedPrivateKeyFile { error, .. } => Some(error), + Self::MalformedPublicKeyFile { error, .. } => Some(error), + Self::MalformedOnDiskKeyPair { error, .. } => Some(error), + Self::MalformedKmipKeyUrl { .. } => None, // TODO + Self::UnreadableKmipServerState { error, .. } => Some(error), + Self::MalformedKmipServerState { error, .. } => Some(error), + Self::KmipClientCredentials { .. } => None, // TODO + Self::MissingKmipClientCredentials { .. } => None, + Self::KmipConnection { .. } => None, // TODO + Self::MalformedKmipKeypair { .. } => None, // TODO + } + } +} + +impl fmt::Display for LoadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoKeysFound => f.write_str("no applicable zone signing keys were found"), + Self::MultipleSchemesInKey { pub_url, priv_url } => { + let pub_scheme = pub_url.scheme(); + let priv_scheme = priv_url.scheme(); + write!( + f, + "Multiple key URI schemes \ + ({pub_scheme:?} and {priv_scheme:?}) \ + as used by key '{pub_url}' are not supported" + ) + } + Self::UnsupportedScheme { url } => { + let scheme = url.scheme(); + write!( + f, + "The key URI scheme '{scheme}', \ + as used by key '{url}', \ + is not supported" + ) + } + Self::UnreadableKeyFile { path, error } => { + write!(f, "Could not load a key from '{path}': {error}") + } + Self::MalformedPrivateKeyFile { path, error } => { + write!( + f, + "The on-disk private key at '{path}' could not be parsed: {error}" + ) + } + Self::MalformedPublicKeyFile { path, error } => { + write!( + f, + "The on-disk public key at '{path}' could not be parsed: {error}" + ) + } + Self::MalformedOnDiskKeyPair { + priv_key_path, + pub_key_path, + error, + } => { + write!( + f, + "An on-disk key-pair \ + (private key '{priv_key_path}', public key '{pub_key_path}') \ + was malformed: {error}" + ) + } + Self::MalformedKmipKeyUrl { url, error } => { + write!(f, "The KMIP key URL '{url}' is malformed: {error}") + } + Self::UnreadableKmipServerState { path, error } => { + write!( + f, + "The KMIP server state file '{path}' could not be read: {error}" + ) + } + Self::MalformedKmipServerState { path, error } => { + write!( + f, + "The KMIP server state file '{path}' was malformed: {error}" + ) + } + Self::KmipClientCredentials { path, error } => { + write!( + f, + "The KMIP client credentials store \ + (at '{path}') could not be loaded: {error}" + ) + } + Self::MissingKmipClientCredentials { server_id, path } => { + write!( + f, + "The KMIP server '{server_id}' requires client credentials, \ + but the client credentials store (at '{path}') does not provide any" + ) + } + Self::KmipConnection { server_id, error } => { + write!(f, "Could not connect to KMIP server '{server_id}': {error}") + } + Self::MalformedKmipKeypair { + priv_key_url, + pub_key_url, + error, + } => { + write!( + f, + "A KMIP key-pair \ + (private key '{priv_key_url}', public key '{pub_key_url}') \ + was malformed: {error}" + ) + } + } + } +} diff --git a/src/signer/mod.rs b/src/signer/mod.rs index 8930ee06..059b7194 100644 --- a/src/signer/mod.rs +++ b/src/signer/mod.rs @@ -34,7 +34,9 @@ use crate::{ units::zone_signer::SignerError, }; +pub mod full; pub mod incremental; +pub mod keys; pub mod queue; pub mod status; pub mod zone; @@ -65,9 +67,11 @@ fn sign( permit: SigningPermit, status: Arc>, ) { - let result = center - .signer - .sign_zone(¢er, &zone, &mut builder, trigger, status.clone()); + let result = if let Some(patcher) = builder.patch() { + self::incremental::sign_incrementally(patcher, &zone, ¢er, trigger, status.clone()) + } else { + self::full::sign_zone(¢er, &zone, &mut builder, trigger, status.clone()) + }; let mut status = status.write().unwrap(); let mut handle = zone.write_handle(¢er); diff --git a/src/units/zone_signer.rs b/src/units/zone_signer.rs index fd23d408..94234507 100644 --- a/src/units/zone_signer.rs +++ b/src/units/zone_signer.rs @@ -1,63 +1,26 @@ -use std::cmp::{Ordering, min}; +use std::cmp::min; use std::collections::{HashMap, HashSet}; use std::env::{self, VarError}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use bytes::Bytes; -use cascade_zonedata::{OldRecord, RegularRecord, SignedZoneBuilder}; use domain::base::Rtype; -use domain::base::Serial; -use domain::base::iana::SecurityAlgorithm; -use domain::base::name::FlattenInto; -use domain::base::{CanonicalOrd, Name, Record}; -use domain::crypto::sign::{SecretKeyBytes, SignRaw}; -use domain::dnssec::common::parse_from_bind; -use domain::dnssec::sign::SigningConfig; -use domain::dnssec::sign::denial::config::DenialConfig; -use domain::dnssec::sign::denial::nsec::generate_nsecs; -use domain::dnssec::sign::denial::nsec3::{ - GenerateNsec3Config, Nsec3ParamTtlMode, Nsec3Records, generate_nsec3s, -}; -use domain::dnssec::sign::error::SigningError; -use domain::dnssec::sign::keys::SigningKey; -use domain::dnssec::sign::keys::keyset::{KeySet, KeyType, UnixTime}; -use domain::dnssec::sign::records::RecordsIter; -use domain::dnssec::sign::signatures::rrsigs::{GenerateRrsigConfig, sign_sorted_zone_records}; -use domain::new::base::{RType, Serial as NewBaseSerial}; -use domain::new::rdata::RecordData; +use domain::dnssec::sign::keys::keyset::{KeySet, UnixTime}; use domain::rdata::dnssec::Timestamp; -use domain::rdata::{Dnskey, Nsec3param, ZoneRecordData}; -use domain::zonefile::inplace::{Entry, Zonefile}; -use domain_kmip::KeyUrl; -use domain_kmip::dep::kmip::client::pool::{ConnectionManager, KmipConnError, SyncConnPool}; +use domain_kmip::dep::kmip::client::pool::SyncConnPool; use domain_kmip::{self, ClientCertificate, ConnectionSettings}; -use jiff::tz::TimeZone; -use jiff::{Timestamp as JiffTimestamp, Zoned}; -use rayon::iter::{ - IntoParallelIterator, IntoParallelRefIterator, ParallelExtend, ParallelIterator, -}; -use rayon::slice::ParallelSliceMut; use serde::{Deserialize, Serialize}; use tokio::sync::watch; use tokio::time::Instant; -use tracing::{debug, error, info, trace, warn}; -use url::Url; +use tracing::trace; use crate::center::Center; -use crate::manager::{Terminated, record_zone_event}; -use crate::policy::{PolicyVersion, SignerDenialPolicy, SignerSerialPolicy}; -use crate::signer::incremental::{LocalState, sign_incrementally}; +use crate::signer::ResigningTrigger; +use crate::signer::keys::LoadError; use crate::signer::queue::SigningQueue; -use crate::signer::status::{SigningStatusPerZone, ZoneSigningStatus}; -use crate::signer::{ResigningTrigger, SigningTrigger}; -use crate::units::http_server::KmipServerState; -use crate::units::key_manager::{ - KmipClientCredentialsFile, KmipServerCredentialsFileMode, mk_dnst_keyset_state_file_path, -}; use crate::util::AbortOnDrop; -use crate::zone::{HistoricalEvent, Zone, ZoneByName}; +use crate::zone::ZoneByName; // Re-signing zones before signatures expire works as follows: // - compute when the first zone needs to be re-signed. Loop over unsigned @@ -141,51 +104,6 @@ impl ZoneSigner { })) } - pub fn load_private_key(key_path: &Path) -> Result { - let private_data = std::fs::read_to_string(key_path).map_err(|err| { - error!("Unable to read file '{}': {err}", key_path.display()); - Terminated - })?; - - // Note: Compared to the original ldns-signzone there is a minor - // regression here because at the time of writing the error returned - // from parsing indicates broadly the type of parsing failure but does - // note indicate the line number at which parsing failed. - let secret_key = SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { - error!( - "Unable to parse BIND formatted private key file '{}': {err}", - key_path.display(), - ); - Terminated - })?; - - Ok(secret_key) - } - - pub fn load_public_key( - key_path: &Path, - ) -> Result, Dnskey>, Terminated> { - let public_data = std::fs::read_to_string(key_path).map_err(|_| { - error!("loading public key from file '{}'", key_path.display(),); - Terminated - })?; - - // Note: Compared to the original ldns-signzone there is a minor - // regression here because at the time of writing the error returned - // from parsing indicates broadly the type of parsing failure but does - // note indicate the line number at which parsing failed. - let public_key_info = parse_from_bind(&public_data).map_err(|err| { - error!( - "Unable to parse BIND formatted public key file '{}': {}", - key_path.display(), - err - ); - Terminated - })?; - - Ok(public_key_info) - } - pub fn on_publish_signed_zone(&self, center: &Arc
) { trace!("[ZS]: a zone is published, recompute next time to re-sign"); let _ = self.next_resign_time_tx.send(self.next_resign_time(center)); @@ -198,490 +116,6 @@ impl ZoneSigner { let _ = self.next_resign_time_tx.send(Some(Instant::now())); } - pub fn sign_zone( - &self, - center: &Arc
, - zone: &Arc, - builder: &mut SignedZoneBuilder, - trigger: SigningTrigger, - status: Arc>, - ) -> Result<(), SignerError> { - let zone_name = &zone.name; - - if let Some(patcher) = builder.patch() { - return sign_incrementally(self, patcher, zone, center, trigger, status); - } - - info!("[ZS]: Starting signing operation for zone '{zone_name}'"); - let start = Instant::now(); - - let mut local_state = LocalState::new(zone)?; - - let policy = { - // Use a block to make sure that the lock is clearly dropped. - let zone_state = zone.read(); - - zone_state.policy.clone().unwrap() - }; - let previous_serial = local_state.previous_serial; - - // - // Lookup the zone to sign. - // - let mut writer = builder.replace().unwrap(); - let mut new_records = Vec::new(); - let loaded = writer - .next_loaded() - .or(writer.curr_loaded()) - .expect("a non-empty loaded instance must exist"); - let loaded_serial = loaded.soa().rdata.serial; - - let serial: Serial = match policy.signer.serial_policy { - SignerSerialPolicy::Keep => { - let loaded_serial = Serial::from(Into::::into(loaded_serial)); - if let Some(previous_serial) = previous_serial - && loaded_serial <= previous_serial - { - return Err(SignerError::KeepSerialPolicyViolated); - } - - loaded_serial - } - SignerSerialPolicy::Counter => { - // Always increment the serial number, ignore the serial - // number in the unsigned zone. - let previous_serial = previous_serial.unwrap_or(Serial::from(0)); - previous_serial.add(1) - } - SignerSerialPolicy::UnixTime => { - let mut serial = Serial::now(); - if let Some(previous_serial) = previous_serial - && serial <= previous_serial - { - serial = previous_serial.add(1); - } - - serial - } - SignerSerialPolicy::DateCounter => { - let ts = JiffTimestamp::now(); - let zone = Zoned::new(ts, TimeZone::UTC); - let serial = ((zone.year() as u32 * 100 + zone.month() as u32) * 100 - + zone.day() as u32) - * 100; - let mut serial: Serial = serial.into(); - - if let Some(previous_serial) = previous_serial - && serial <= previous_serial - { - serial = previous_serial.add(1); - } - - serial - } - }; - local_state.previous_serial = Some(serial); - let serial = NewBaseSerial::from(serial.into_int()); - let new_soa = { - let mut soa = loaded.soa().clone(); - soa.rdata.serial = serial; - soa - }; - new_records.push(new_soa.clone().into()); - - info!( - "[ZS]: Serials for zone '{zone_name}': last signed={previous_serial:?}, current={loaded_serial}, serial policy={}, new={serial}", - policy.signer.serial_policy - ); - - // - // Record the start of signing for this zone. - // - { - status - .write() - .unwrap() - .status - .start(loaded_serial) - .map_err(|_| SignerError::InternalError("Invalid status".to_string()))?; - } - - // - // Create a signing configuration. - // - let signing_config = self.signing_config(&policy)?; - let rrsig_cfg = - GenerateRrsigConfig::new(signing_config.inception, signing_config.expiration); - - // - // Convert zone records into a form we can sign. - // - status.write().unwrap().current_action = "Collecting records to sign".to_string(); - debug!("[ZS]: Collecting records to sign for zone '{zone_name}'."); - let walk_start = Instant::now(); - let mut records = loaded - .unsigned_records() - .filter(|r| r.rname != new_soa.rname || r.rtype != new_soa.rtype) - .cloned() - .map(OldRecord::from) - .collect::>(); - records.push(new_soa.clone().into()); - let walk_time = walk_start.elapsed(); - let unsigned_rr_count = records.len(); - - { - let mut v = status.write().unwrap(); - let v2 = &mut v.status; - if let ZoneSigningStatus::InProgress(s) = v2 { - s.unsigned_rr_count = Some(unsigned_rr_count); - s.walk_time = Some(walk_time); - } - } - - debug!("Reading dnst keyset DNSKEY RRs and RRSIG RRs"); - status.write().unwrap().current_action = - "Fetching apex RRs from the key manager".to_string(); - // Read the DNSKEY RRs and DNSKEY RRSIG RR from the keyset state. - let state_path = mk_dnst_keyset_state_file_path(¢er.config.keys_dir, &zone.name); - let state = std::fs::read_to_string(&state_path) - .map_err(|_| SignerError::CannotReadStateFile(state_path.into_string()))?; - let state: KeySetState = serde_json::from_str(&state).unwrap(); - - local_state.apex_remove = state.apex_remove.clone(); - let mut apex_extra = state.apex_extra.clone(); - apex_extra.sort(); - local_state.apex_extra = apex_extra; - - for rr in &state.apex_extra { - let mut zonefile = Zonefile::new(); - zonefile.extend_from_slice(rr.as_bytes()); - zonefile.extend_from_slice(b"\n"); - if let Ok(Some(Entry::Record(rec))) = zonefile.next_entry() { - let record: OldRecord = rec.flatten_into(); - new_records.push(record.clone().into()); - records.push(record); - } - } - - debug!("Loading dnst keyset signing keys"); - status.write().unwrap().current_action = "Loading signing keys".to_string(); - // Load the signing keys indicated by the keyset state. - let signing_keys = load_keys(self, center, zone_name.clone(), &state, status.clone())?; - - debug!("{} signing keys loaded", signing_keys.len()); - - // TODO: If signing is disabled for a zone should we then allow the - // unsigned zone to propagate through the pipeline? - if signing_keys.is_empty() { - warn!("No signing keys found for zone {zone_name}, aborting"); - return Err(SignerError::SigningError( - "No signing keys found".to_string(), - )); - } - - // Save the current zone signing keys and clear key_roll - let mut key_tags = HashSet::new(); - for v in state.keyset.keys().values() { - let signer = match v.keytype() { - KeyType::Ksk(_) => false, - KeyType::Zsk(key_state) => key_state.signer(), - KeyType::Csk(_, key_state) => key_state.signer(), - KeyType::Include(_) => false, - }; - - if !signer { - continue; - } - - key_tags.insert(v.key_tag()); - } - local_state.key_tags = key_tags; - local_state.key_roll = None; - - // - // Sort them into DNSSEC order ready for NSEC(3) generation. - // - debug!("[ZS]: Sorting collected records for zone '{zone_name}'."); - status.write().unwrap().current_action = "Sorting records".to_string(); - let sort_start = Instant::now(); - // Note: This may briefly use lots of CPU and many CPU cores. - records.par_sort_by(CanonicalOrd::canonical_cmp); - let sort_time = sort_start.elapsed(); - let unsigned_rr_count = records.len(); - - { - let mut v = status.write().unwrap(); - let v2 = &mut v.status; - if let ZoneSigningStatus::InProgress(s) = v2 { - s.sort_time = Some(sort_time); - } - } - - // - // Generate NSEC(3) RRs. - // - debug!("[ZS]: Generating denial records for zone '{zone_name}'."); - status.write().unwrap().current_action = "Generating denial records".to_string(); - let denial_start = Instant::now(); - match &signing_config.denial { - DenialConfig::AlreadyPresent => {} - - DenialConfig::Nsec(cfg) => { - let nsecs = generate_nsecs(&zone.name, RecordsIter::new_from_owned(&records), cfg) - .map_err(|err: SigningError| { - SignerError::SigningError(format!("Failed to generate denial RRs: {err}")) - })?; - - new_records.par_extend( - nsecs - .par_iter() - .map(|r| OldRecord::from_record(r.clone()).into()), - ); - records.par_extend(nsecs.into_par_iter().map(Record::from_record)); - } - - DenialConfig::Nsec3(cfg) => { - // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash - // order." We store the NSEC3s as we create them and sort them - // afterwards. - let Nsec3Records { nsec3s, nsec3param } = - generate_nsec3s(&zone.name, RecordsIter::new_from_owned(&records), cfg) - .map_err(|err: SigningError| { - SignerError::SigningError(format!( - "Failed to generate denial RRs: {err}" - )) - })?; - - // Add the generated NSEC3 records. - new_records.par_extend( - nsec3s - .par_iter() - .map(|r| OldRecord::from_record(r.clone()).into()), - ); - new_records.push(OldRecord::from_record(nsec3param.clone()).into()); - records.par_extend(nsec3s.into_par_iter().map(Record::from_record)); - records.push(Record::from_record(nsec3param)); - } - } - // Use a stable sort; the stable sort algorithm detects runs of sorted - // elements ('records' contains two concatenated pre-sorted runs) and - // can efficiently sort around them. - records.par_sort_by(CanonicalOrd::canonical_cmp); - - let unsigned_records = records; - let denial_time = denial_start.elapsed(); - let denial_rr_count = unsigned_records.len() - unsigned_rr_count; - - { - let mut v = status.write().unwrap(); - let v2 = &mut v.status; - if let ZoneSigningStatus::InProgress(s) = v2 { - s.denial_rr_count = Some(denial_rr_count); - s.denial_time = Some(denial_time); - } - } - - // - // Generate RRSIG RRs concurrently. - // - // Use N concurrent Rayon scoped threads to do blocking RRSIG - // generation without interfering with Tokio task scheduling, and an - // async task which receives generated RRSIGs via a Tokio - // mpsc::channel and accumulates them into the signed zone. - // - debug!("[ZS]: Generating RRSIG records."); - status.write().unwrap().current_action = "Generating signature records".to_string(); - - // TODO: Configure Rayon's thread pool to set the number of threads. By - // default, it relies on 'std::thread::available_parallelism()'. - let parallelism = rayon::current_num_threads(); - - { - let mut v = status.write().unwrap(); - let v2 = &mut v.status; - if let ZoneSigningStatus::InProgress(s) = v2 { - s.threads_used = Some(parallelism); - } - } - - let generation_start = Instant::now(); - - // Get the keys to sign with. Domain's 'sign_sorted_zone_records()' - // needs a slice of references, so we need to build that here. - let keys = signing_keys.iter().collect::>(); - - // TODO: This generation code is incorrect; 'sign_sorted_zone_records' - // looks for zone cuts, but zone cuts may need to be detected _across_ - // the segments we split the records into. Zone cut detection needs to - // be re-implemented here with parallel execution in mind. This also - // applies to NSEC(3) generation, but it is currently single-threaded. - - // Disable parallel signing for now. This may also split RRsets. - let signatures = if false { - // Split the records into segments. - let segments = rayon::iter::split(0..unsigned_records.len(), |range| { - // Always sign at least 1024 records at a time. - if range.len() < 1024 { - return (range, None); - } - - let midpoint = range.start + range.len() / 2; - let left = range.start..midpoint; - let right = midpoint..range.end; - (left, Some(right)) - }); - - // Generate signatures from each segment. - let signatures = segments.map(|range| { - sign_sorted_zone_records( - &zone.name, - RecordsIter::new_from_owned(&unsigned_records[range]), - &keys, - &rrsig_cfg, - ) - }); - - // Convert the signatures into new-base types and collect them together. - // If errors occur, one error is arbitrarily chosen and returned. - signatures - .try_fold(Vec::new, |mut a, b| { - a.extend(b?.into_iter().map(|r| OldRecord::from_record(r).into())); - Ok::<_, SigningError>(a) - }) - .try_reduce(Vec::new, |mut a, mut b| { - a.append(&mut b); - Ok(a) - }) - .map_err(|err| SignerError::SigningError(err.to_string()))? - } else { - let signatures = sign_sorted_zone_records( - &zone.name, - RecordsIter::new_from_owned(&unsigned_records), - &keys, - &rrsig_cfg, - ) - .map_err(|err| SignerError::SigningError(err.to_string()))?; - let signatures: Vec = signatures - .into_iter() - .map(|s| { - let r = Record::new( - s.owner().clone(), - s.class(), - s.ttl(), - ZoneRecordData::Rrsig(s.data().clone()), - ); - r.into() - }) - .collect(); - signatures - }; - - let total_signatures = signatures.len(); - - new_records.extend(signatures); - new_records.par_sort(); - writer.set_records(new_records).unwrap(); - - let generation_time = generation_start.elapsed(); - - let generation_rate = total_signatures as f64 / generation_time.as_secs_f64().min(0.001); - - writer.set_soa(new_soa.clone()).unwrap(); - writer.apply().unwrap(); - - debug!("SIGNER: Determining min expiration time"); - let reader = builder.next_signed().unwrap(); - let min_expiration = Arc::new(MinTimestamp::new()); - let saved_min_expiration = min_expiration.clone(); - for record in reader.generated_records() { - let RecordData::Rrsig(sig) = record.rdata.get() else { - continue; - }; - - // Ignore RRSIG records for DNSKEY, CDS, and CDNSKEY records; these - // are generated by the key manager, using KSKs. - if sig.rtype == RType::DNSKEY - || sig.rtype == RType::from(59) - || sig.rtype == RType::from(60) - { - continue; - } - - min_expiration.add(u32::from(sig.expiration).into()); - } - local_state.next_min_expiration = saved_min_expiration.get(); - - let total_time = start.elapsed(); - - { - let mut v = status.write().unwrap(); - let v2 = &mut v.status; - if let ZoneSigningStatus::InProgress(s) = v2 { - s.rrsig_count = Some(total_signatures); - s.rrsig_reused_count = Some(0); // Not implemented yet - s.rrsig_time = Some(generation_time); - s.total_time = Some(total_time); - } - v.status.finish(true); - } - - // Log signing statistics. - info!( - "Signing statistics for {zone_name} serial: {serial}:\n\ - Collected {unsigned_rr_count} records in {:.1}s, sorted in {:.1}s\n\ - Generated {denial_rr_count} NSEC(3) records in {:.1}s\n\ - Generated {total_signatures} signatures in {:.1}s ({generation_rate:.0}sig/s) - Took {:.1}s in total, using {parallelism} threads", - walk_time.as_secs_f64(), - sort_time.as_secs_f64(), - denial_time.as_secs_f64(), - generation_time.as_secs_f64(), - total_time.as_secs_f64() - ); - - record_zone_event( - center, - zone, - HistoricalEvent::SigningSucceeded { - trigger: trigger.into(), - }, - Some(domain::base::Serial(serial.into())), - ); - - local_state.last_signature_refresh = UnixTime::now(); - local_state.save(center, zone); - - Ok(()) - } - - fn signing_config( - &self, - policy: &PolicyVersion, - ) -> Result, SignerError> { - let denial = match &policy.signer.denial { - SignerDenialPolicy::NSec => DenialConfig::Nsec(Default::default()), - SignerDenialPolicy::NSec3 { opt_out } => { - let first = parse_nsec3_config(*opt_out); - DenialConfig::Nsec3(first) - } - }; - - let now = match env::var("CASCADE_FAKETIME") { - Ok(val) => val - .parse::() - .map_err(|e| SignerError::InternalError(format!("cannot parse {e} as u32")))?, - Err(VarError::NotPresent) => Timestamp::now().into_int(), - Err(e) => return Err(SignerError::InternalError(e.to_string())), - }; - let inception = now.wrapping_sub(policy.signer.sig_inception_offset); - let expiration = now.wrapping_add(policy.signer.sig_validity_time); - Ok(SigningConfig::new( - denial, - inception.into(), - expiration.into(), - )) - } - fn next_resign_time(&self, center: &Arc
) -> Option { let mut min_time = None; @@ -857,76 +291,12 @@ impl Default for MinTimestamp { } } -fn parse_nsec3_config(opt_out: bool) -> GenerateNsec3Config { - let mut params = Nsec3param::default(); - if opt_out { - params.set_opt_out_flag() - } - - // TODO: support other ttl_modes? Seems missing from the config right now - let ttl_mode = Nsec3ParamTtlMode::Soa; - GenerateNsec3Config::new(params).with_ttl_mode(ttl_mode) -} - impl std::fmt::Debug for ZoneSigner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ZoneSigner").finish() } } -//----------- KeyPair ---------------------------------------------------------- - -/// A cryptographic keypair for signing. -#[derive(Debug)] -pub enum KeyPair { - /// A keypair provided by [`domain`]. - Domain(domain::crypto::sign::KeyPair), - - /// A KMIP keypair. - Kmip(domain_kmip::sign::KeyPair), -} - -impl SignRaw for KeyPair { - fn algorithm(&self) -> SecurityAlgorithm { - match self { - KeyPair::Domain(k) => k.algorithm(), - KeyPair::Kmip(k) => k.algorithm(), - } - } - - fn dnskey(&self) -> Dnskey> { - match self { - KeyPair::Domain(k) => k.dnskey(), - KeyPair::Kmip(k) => k.dnskey(), - } - } - - fn sign_raw( - &self, - data: &[u8], - ) -> Result { - match self { - KeyPair::Domain(k) => k.sign_raw(data), - KeyPair::Kmip(k) => k.sign_raw(data), - } - } -} - -//------------ MultiThreadedSorter ------------------------------------------- - -/// A parallelized sort implementation for signing. -struct MultiThreadedSorter; - -impl domain::dnssec::sign::records::Sorter for MultiThreadedSorter { - fn sort_by(records: &mut Vec>, compare: F) - where - F: Fn(&Record, &Record) -> Ordering + Sync, - Record: CanonicalOrd + Send, - { - records.par_sort_by(compare); - } -} - //------------ KMIP related -------------------------------------------------- #[derive(Clone, Debug)] @@ -1035,206 +405,6 @@ pub fn load_binary_file(path: &Path) -> Vec { bytes } -pub fn load_keys( - zone_signer: &ZoneSigner, - center: &Arc
, - zone_name: Name, - keyset_state: &KeySetState, - status: Arc>, -) -> Result>, SignerError> { - debug!("Loading dnst keyset signing keys"); - - let kmip_server_state_dir = ¢er.config.kmip_server_state_dir; - let kmip_credentials_store_path = ¢er.config.kmip_credentials_store_path; - - debug!("Reading dnst keyset DNSKEY RRs and RRSIG RRs"); - status.write().unwrap().current_action = "Fetching apex RRs from the key manager".to_string(); - - // Read the DNSKEY RRs and DNSKEY RRSIG RR from the keyset state. - - status.write().unwrap().current_action = "Loading signing keys".to_string(); - // Load the signing keys indicated by the keyset state. - let mut signing_keys = vec![]; - for (pub_key_name, key_info) in keyset_state.keyset.keys() { - // Only use active ZSKs or CSKs to sign the records in the zone. - if !matches!(key_info.keytype(), - KeyType::Zsk(key_state) - | KeyType::Csk(_, key_state) if key_state.signer()) - { - continue; - } - - if let Some(priv_key_name) = key_info.privref() { - let priv_url = Url::parse(priv_key_name).expect("valid URL expected"); - let pub_url = Url::parse(pub_key_name).expect("valid URL expected"); - - match (priv_url.scheme(), pub_url.scheme()) { - ("file", "file") => { - let priv_key_path = priv_url.path(); - debug!("Attempting to load private key '{priv_key_path}'."); - - let private_key = ZoneSigner::load_private_key(Path::new(priv_key_path)) - .map_err(|_| { - SignerError::CannotReadPrivateKeyFile(priv_key_path.to_string()) - })?; - - let pub_key_path = pub_url.path(); - debug!("Attempting to load public key '{pub_key_path}'."); - - let public_key = - ZoneSigner::load_public_key(Path::new(pub_key_path)).map_err(|_| { - SignerError::CannotReadPublicKeyFile(pub_key_path.to_string()) - })?; - - let key_pair = - domain::crypto::sign::KeyPair::from_bytes(&private_key, public_key.data()) - .map_err(|err| { - SignerError::InvalidKeyPairComponents(err.to_string()) - })?; - let signing_key = SigningKey::new( - zone_name.clone(), - public_key.data().flags(), - KeyPair::Domain(key_pair), - ); - - signing_keys.push(signing_key); - } - - ("kmip", "kmip") => { - let priv_key_url = - KeyUrl::try_from(priv_url).map_err(SignerError::InvalidPublicKeyUrl)?; - let pub_key_url = - KeyUrl::try_from(pub_url).map_err(SignerError::InvalidPrivateKeyUrl)?; - - // TODO: Replace the connection pool if the persisted KMIP server settings - // were updated more recently than the pool was created. - - let mut kmip_servers = zone_signer.kmip_servers.lock().unwrap(); - let kmip_conn_pool = match kmip_servers - .entry(priv_key_url.server_id().to_string()) - { - std::collections::hash_map::Entry::Occupied(e) => e.into_mut(), - std::collections::hash_map::Entry::Vacant(e) => { - // Try and load the KMIP server settings. - let p = kmip_server_state_dir.join(priv_key_url.server_id()); - info!("Reading KMIP server state from '{p}'"); - let f = std::fs::File::open(p).unwrap(); - let kmip_server: KmipServerState = serde_json::from_reader(f).unwrap(); - let KmipServerState { - server_id, - ip_host_or_fqdn: host, - port, - insecure, - connect_timeout, - read_timeout, - write_timeout, - max_response_bytes, - has_credentials, - .. - } = kmip_server; - - let mut username = None; - let mut password = None; - if has_credentials { - let creds_file = KmipClientCredentialsFile::new( - kmip_credentials_store_path.as_std_path(), - KmipServerCredentialsFileMode::ReadOnly, - ) - .unwrap(); - - let creds = creds_file.get(&server_id).ok_or( - SignerError::KmipServerCredentialsNeeded(server_id.clone()), - )?; - - username = Some(creds.username.clone()); - password = creds.password.clone(); - } - - let conn_settings = ConnectionSettings { - host, - port, - username, - password, - insecure, - client_cert: None, // TODO - server_cert: None, // TODO - ca_cert: None, // TODO - connect_timeout: Some(connect_timeout), - read_timeout: Some(read_timeout), - write_timeout: Some(write_timeout), - max_response_bytes: Some(max_response_bytes), - }; - - let cloned_status = status.clone(); - let cloned_server_id = server_id.clone(); - tokio::task::spawn(async move { - cloned_status.write().unwrap().current_action = - format!("Connecting to KMIP server '{cloned_server_id}"); - }); - let pool = ConnectionManager::create_connection_pool( - server_id.clone(), - Arc::new(conn_settings.clone()), - 10, - Some(Duration::from_secs(60)), - Some(Duration::from_secs(60)), - ) - .map_err(|err| { - SignerError::CannotCreateKmipConnectionPool(server_id, err) - })?; - - e.insert(pool) - } - }; - - let _flags = priv_key_url.flags(); - - let cloned_status = status.clone(); - let cloned_server_id = priv_key_url.server_id().to_string(); - tokio::task::spawn(async move { - cloned_status.write().unwrap().current_action = - format!("Fetching keys from KMIP server '{cloned_server_id}'"); - }); - - let key_pair = KeyPair::Kmip( - domain_kmip::sign::KeyPair::from_urls( - priv_key_url, - pub_key_url, - kmip_conn_pool.clone(), - ) - .map_err(|err| SignerError::InvalidKeyPairComponents(err.to_string()))?, - ); - - let signing_key = - SigningKey::new(zone_name.clone(), key_pair.dnskey().flags(), key_pair); - - signing_keys.push(signing_key); - } - - (other1, other2) => { - return Err(SignerError::InvalidKeyPairComponents(format!( - "Using different key URI schemes ({other1} vs {other2}) for a public/private key pair is not supported." - ))); - } - } - - debug!("Loaded key pair for zone {zone_name} from key pair"); - } - } - - debug!("{} signing keys loaded", signing_keys.len()); - - // TODO: If signing is disabled for a zone should we then allow the - // unsigned zone to propagate through the pipeline? - if signing_keys.is_empty() { - warn!("No signing keys found for zone {zone_name}, aborting"); - return Err(SignerError::SigningError( - "No signing keys found".to_string(), - )); - } - - Ok(signing_keys) -} - pub fn faketime_or_now() -> UnixTime { match env::var("CASCADE_FAKETIME") { Ok(val) => val.parse::().unwrap().into(), @@ -1266,13 +436,7 @@ pub enum SignerError { InternalError(String), KeepSerialPolicyViolated, CannotReadStateFile(String), - CannotReadPrivateKeyFile(String), - CannotReadPublicKeyFile(String), - InvalidKeyPairComponents(String), - InvalidPublicKeyUrl(String), - InvalidPrivateKeyUrl(String), - KmipServerCredentialsNeeded(String), - CannotCreateKmipConnectionPool(String, KmipConnError), + Load(String), PatchFailed(String), NothingToDo, SigningError(String), @@ -1290,36 +454,16 @@ impl std::fmt::Display for SignerError { SignerError::CannotReadStateFile(path) => { write!(f, "Failed to read state file '{path}'") } - SignerError::CannotReadPrivateKeyFile(path) => { - write!(f, "Failed to read private key file '{path}'") - } - SignerError::CannotReadPublicKeyFile(path) => { - write!(f, "Failed to read public key file '{path}'") - } - SignerError::InvalidKeyPairComponents(err) => { - write!( - f, - "Failed to create a key pair from private and public keys: {err}" - ) - } - SignerError::InvalidPublicKeyUrl(err) => { - write!(f, "Invalid public key URL: {err}") - } - SignerError::InvalidPrivateKeyUrl(err) => { - write!(f, "Invalid private key URL: {err}") - } - SignerError::KmipServerCredentialsNeeded(server_id) => { - write!(f, "No credentials available for KMIP server '{server_id}'") - } - SignerError::CannotCreateKmipConnectionPool(server_id, err) => { - write!( - f, - "Cannot create connection pool for KMIP server '{server_id}': {err}" - ) - } + SignerError::Load(err) => write!(f, "Could not load the signing keys: {err}"), SignerError::PatchFailed(err) => write!(f, "Patch failed: {err}"), SignerError::NothingToDo => write!(f, "Nothing To Do"), SignerError::SigningError(err) => write!(f, "Signing error: {err}"), } } } + +impl From> for SignerError { + fn from(error: Box) -> Self { + Self::Load(error.to_string()) + } +}