//! Certificate authorities. use std::{fmt, ops, str}; use std::collections::hash_map; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; use base64::engine::Engine as _; use bytes::Bytes; use chrono::{Duration, TimeZone, Utc}; use rpki::uri; use rpki::ca::idcert::IdCert; use rpki::ca::idexchange::{ CaHandle, ChildHandle, ParentHandle, RepoInfo, ServiceUri, }; use rpki::ca::provisioning::{ IssuanceRequest, IssuedCert, RequestResourceLimit, ResourceClassEntitlements, ResourceClassListResponse, ResourceClassName, }; use rpki::ca::publication::{Base64, PublishDelta, PublishDeltaElement}; use rpki::crypto::{KeyIdentifier, PublicKey}; use rpki::repository::aspa::Aspa; use rpki::repository::cert::Cert; use rpki::repository::crl::{Crl, CrlEntry}; use rpki::repository::manifest::Manifest; use rpki::repository::resources::{Asn, ResourceSet}; use rpki::repository::roa::Roa; use rpki::repository::x509::{Name, Serial, Time, Validity}; use rpki::rrdp::Hash; use serde::{Deserialize, Serialize}; use crate::commons::crypto::CsrInfo; use crate::commons::error; use crate::commons::version::KrillVersion; use super::admin::{PublishedFile, RepositoryContact}; use super::aspa::AspaDefinition; use super::bgpsec::BgpSecAsnKey; use super::status::ErrorResponse; use super::roa::{RoaPayload, RoaPayloadJsonMapKey}; //------------ IdCertInfo ---------------------------------------------------- /// An encoded ID certificate and SHA256 hash of the encoding. // // *Warning:* This type is used in stored state. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct IdCertInfo { /// The public key of the ID certificate. pub public_key: PublicKey, /// The enocoded ID certificate. pub base64: Base64, /// The SHA-256 hash over the ID certificate. pub hash: Hash, } impl IdCertInfo { /// Returns the PEM encoding of the certificate. pub fn pem(&self) -> IdCertPem { IdCertPem { base64: &self.base64 } } } impl From<&IdCert> for IdCertInfo { fn from(cer: &IdCert) -> Self { let bytes = cer.to_bytes(); IdCertInfo { public_key: cer.public_key().clone(), base64: Base64::from_content(&bytes), hash: Hash::from_data(&bytes), } } } impl From for IdCertInfo { fn from(cer: IdCert) -> Self { Self::from(&cer) } } impl TryFrom<&IdCertInfo> for IdCert { type Error = error::Error; fn try_from(info: &IdCertInfo) -> Result { IdCert::decode(info.base64.to_bytes().as_ref()).map_err(|e| { error::Error::Custom(format!( "Could not decode IdCertInfo into IdCert: {e}" )) }) } } impl fmt::Display for IdCertInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{}", self.pem()) } } //------------ IdCertPem ----------------------------------------------------- /// A helper type for writing a PEM-encoded ID certifiate. /// /// A value of this type is returned by [`IdCertInfo::pem`]. pub struct IdCertPem<'a> { base64: &'a Base64, } impl fmt::Display for IdCertPem<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("-----BEGIN CERTIFICATE-----\n")?; for line in self .base64 .as_str() .as_bytes() // so we can use chunks .chunks(64) .map(|b| unsafe { std::str::from_utf8_unchecked(b) }) { f.write_str(line)?; f.write_str("\n")?; } f.write_str("-----END CERTIFICATE-----\n") } } //------------ ChildState ---------------------------------------------------- /// The suspension status of a child CA. // // *Warning:* This type is used in stored state. #[derive( Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, )] #[serde(rename_all = "snake_case")] pub enum ChildState { /// The child CA is active, i.e., not suspended. #[default] Active, /// The child CA has been suspended. Suspended, } impl ChildState { /// Returns whether the state is suspended. pub fn is_suspended(self) -> bool { matches!(self, Self::Suspended) } } impl fmt::Display for ChildState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str( match &self { ChildState::Active => "active", ChildState::Suspended => "suspended", } ) } } //------------ ChildCaInfo --------------------------------------------------- /// Information about a child CA. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ChildCaInfo { /// The child CA’s status vis-a-vis suspension. pub state: ChildState, /// The ID certificate used by the child CA for communication. pub id_cert: IdCertInfo, /// The resources set assigned to the child CA. pub entitled_resources: ResourceSet, } impl fmt::Display for ChildCaInfo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", self.id_cert.pem())?; writeln!( f, "SHA256 hash of PEM encoded certificate: {}", self.id_cert.hash )?; writeln!(f, "resources: {}", self.entitled_resources)?; writeln!(f, "state: {}", self.state) } } //------------ ReceivedCert -------------------------------------------------- /// A marker indicating that a certificate has been received from a parent CA. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Received; /// A certificate that was received from a parent CA. // // *Warning:* This type is used in stored state. pub type ReceivedCert = CertInfo; //------------ IssuedCertificate --------------------------------------------- /// A marker indicating that a certificate has been issued to a child CA. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Issued; /// A certificate which has been issued to a child CA. // // *Warning:* This type is used in stored state. pub type IssuedCertificate = CertInfo; //------------ SuspendedCertificate ------------------------------------------ /// A marker indicating that a certificate has been suspended. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Suspended; /// An certificate which has been suspended because the child is inactive. // // *Warning:* This type is used in stored state. pub type SuspendedCert = CertInfo; //------------ UnsuspendedCertificate ---------------------------------------- /// A marker indicating that a certificate needs to be re-activated. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Unsuspended; /// A certificate that has been unsuspended and needs to be re-activated. // // *Warning:* This type is used in stored state. pub type UnsuspendedCert = CertInfo; //------------ CertInfo ------------------------------------------------------ /// All information about an RPKI CA certificate. /// /// For robustness, we keep all information about the certificate in this /// separate type rather than just storing the final certificate. /// /// This type is generic over a marker type `T` indicating the status of the /// certificate. // // *Warning:* This type is used in stored state. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct CertInfo { /// Where this certificate is published by the parent pub uri: uri::Rsync, /// The name of this certificate as used on a manifest pub name: ObjectName, /// The resources assigned to the CA. pub resources: ResourceSet, /// The resource limit on the signing request. /// /// The default is to have no limit. pub limit: RequestResourceLimit, /// The subject chosen by the parent. /// /// Note that Krill will derive the subject from the public key, but /// other parents may use a different strategy. pub subject: Name, /// The validity time for this certificate. pub validity: Validity, /// The serial number of this certificate. /// /// This is needed for revocatio. pub serial: Serial, /// The certifcate signing request for the certificate. /// /// This contains the public key and SIA. #[serde(flatten)] pub csr_info: CsrInfo, /// The actual encoded certificate. pub base64: Base64, /// The SHA-256 hash of the encoded certificate. pub hash: Hash, /// Marker for the certificate type. marker: std::marker::PhantomData, } impl CertInfo { /// Creates a new value from all parts. pub fn create( cert: Cert, uri: uri::Rsync, resources: ResourceSet, limit: RequestResourceLimit, ) -> Result { let name = { let path = uri.path(); let after_last_slash = path.rfind('/').unwrap_or(0) + 1; // certificate file names must end with .cer and have at least // one more character before the .cer filename extension - i.e. we // expect 5 characters after the last slash. if !path.ends_with(".cer") || path.len() < after_last_slash + 5 { Err(InvalidCert::Uri(uri.clone())) } else { Ok(ObjectName(path[after_last_slash..].into())) } }?; let key = cert.subject_public_key_info().clone(); let ca_repository = cert .ca_repository() .ok_or(InvalidCert::CaRepositoryMissing)? .clone(); let rpki_manifest = cert .rpki_manifest() .ok_or(InvalidCert::RpkiManifestMissing)? .clone(); let rpki_notify = cert.rpki_notify().cloned(); let csr_info = CsrInfo::new(ca_repository, rpki_manifest, rpki_notify, key); let subject = cert.subject().clone(); let validity = cert.validity(); let serial = cert.serial_number(); let base64 = Base64::from(&cert); let hash = base64.to_hash(); base64.to_hash(); Ok(CertInfo { uri, name, resources, limit, subject, validity, serial, csr_info, base64, hash, marker: std::marker::PhantomData, }) } /// Returns the key identifier for the certificate’s public key. pub fn key_identifier(&self) -> KeyIdentifier { self.csr_info.key_id() } /// Returns the expiry time of the certificate. pub fn expires(&self) -> Time { self.validity.not_after() } /// Decodes the certificate. pub fn to_cert(&self) -> Result { Cert::decode( self.to_bytes().as_ref() ).map_err(|e| CertInfoDecodeError(e.to_string())) } /// Converts the CA certificate to an RFC 6492 issued certificate. pub fn to_rfc6492_issued_cert( &self ) -> Result { let cert = self.to_cert()?; Ok(IssuedCert::new(self.uri.clone(), self.limit.clone(), cert)) } /// Returns the raw bytes of the encoded certificate. pub fn to_bytes(&self) -> Bytes { self.base64.to_bytes() } /// Clones and then converts this into a certificate of another type. pub fn to_converted(&self) -> CertInfo { CertInfo { uri: self.uri.clone(), name: self.name.clone(), resources: self.resources.clone(), limit: self.limit.clone(), subject: self.subject.clone(), validity: self.validity, serial: self.serial, csr_info: self.csr_info.clone(), base64: self.base64.clone(), hash: self.hash, marker: std::marker::PhantomData, } } /// Converts this into a certificate of another type. pub fn into_converted(self) -> CertInfo { CertInfo { uri: self.uri, name: self.name, resources: self.resources, limit: self.limit, subject: self.subject, validity: self.validity, serial: self.serial, csr_info: self.csr_info, base64: self.base64, hash: self.hash, marker: std::marker::PhantomData, } } /// Returns a set of reduced applicable resources. /// /// This set is the intersection of the encompassing resources and this /// certificate's current resources. /// /// Returns `None` if the current resource set is not overclaiming and /// does not need to be reduced. pub fn reduced_applicable_resources( &self, encompassing: &ResourceSet, ) -> Option { if encompassing.contains(&self.resources) { None } else { Some(encompassing.intersection(&self.resources)) } } /// Returns the name of the CRL published by this certificate. pub fn crl_name(&self) -> ObjectName { ObjectName::from_key(&self.key_identifier(), "crl") } /// Returns the URI of the CRL published by this certificate. /// /// This is the URI to use on certs issued by this certificate. pub fn crl_uri(&self) -> uri::Rsync { self.uri_for_object(self.crl_name()) } /// Returns the name of the manifest published by this certificate. pub fn mft_name(&self) -> ObjectName { ObjectName::from_key(&self.key_identifier(), "mft") } /// Returns the URI of the manifest published by this certificate. pub fn mft_uri(&self) -> uri::Rsync { self.uri_for_object(self.mft_name()) } /// Returns the CA repository URI where this certificate publishes. pub fn ca_repository(&self) -> &uri::Rsync { self.csr_info.ca_repository() } /// Returns the URI for an object published by this CA. pub fn uri_for_object(&self, name: impl Into) -> uri::Rsync { self.uri_for_name(&name.into()) } /// Returns the URI for an object published by this CA. pub fn uri_for_name(&self, name: &ObjectName) -> uri::Rsync { // unwraps here are safe self.ca_repository().join(name.as_ref()).unwrap() } /// Returns the revocation information for this certificate pub fn revocation(&self) -> Revocation { Revocation::new(self.serial, self.validity.not_after()) } } //------------ PendingKeyInfo ------------------------------------------------ /// Information about a pending key in a resource class. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct PendingKeyInfo { /// The key identifier of the pending key. pub key_id: KeyIdentifier, } //------------ CertifiedKeyInfo ---------------------------------------------- /// Information about a certified key. /// /// Such a key has received an incoming certificate and has at least a /// manifest and CRL. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct CertifiedKeyInfo { /// The key identifier of the key. pub key_id: KeyIdentifier, /// The certificate received from the parent CA. pub incoming_cert: ReceivedCert, /// The certification request sent to the parent if available. pub request: Option, } //------------ ObjectName ---------------------------------------------------- /// Represents the (deterministic) file names of an RPKI repository object. /// /// Values of this type can be cloned relatively cheaply. They contain the /// allocated name behind an arc. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ObjectName(Arc); impl ObjectName { /// Creates a new object. pub fn new(name: impl Into>) -> Self { Self(name.into()) } /// Creates a new object name from a key identifer and a file extension. pub fn from_key(ki: &KeyIdentifier, extension: &str) -> Self { ObjectName(format!("{ki}.{extension}").into()) } /// Creates the name for a CA certificate from its key. pub fn cer_from_key(ki: &KeyIdentifier) -> Self { ObjectName::from_key(ki, "cer") } /// Creates the name of a manifest from the key of its CA. pub fn mft_from_ca_key(ki: &KeyIdentifier) -> Self { ObjectName::from_key(ki, "mft") } /// Creates the name of a CRL from the key of its CA. pub fn crl_from_ca_key(ki: &KeyIdentifier) -> Self { ObjectName::from_key(ki, "crl") } /// Creates the name of an ASPA object from the customer ASN. pub fn aspa_from_customer(customer: Asn) -> Self { ObjectName(format!("{customer}.asa").into()) } /// Creates the name of a router key from ASN and key identifer. pub fn bgpsec(asn: Asn, key: KeyIdentifier) -> Self { ObjectName( format!("ROUTER-{:08X}-{}.cer", asn.into_u32(), key).into(), ) } } impl From<&Cert> for ObjectName { fn from(c: &Cert) -> Self { Self::cer_from_key(&c.subject_key_identifier()) } } impl From<&Manifest> for ObjectName { fn from(m: &Manifest) -> Self { Self::mft_from_ca_key(&m.cert().authority_key_identifier().unwrap()) } } impl From<&Crl> for ObjectName { fn from(c: &Crl) -> Self { Self::crl_from_ca_key(c.authority_key_identifier()) } } impl From for ObjectName { fn from(auth: RoaPayloadJsonMapKey) -> Self { ObjectName(format!("{}.roa", hex::encode(auth.to_string())).into()) } } impl From for ObjectName { fn from(def: RoaPayload) -> Self { ObjectName(format!("{}.roa", hex::encode(def.to_string())).into()) } } impl From<&AspaDefinition> for ObjectName { fn from(aspa: &AspaDefinition) -> Self { Self::aspa_from_customer(aspa.customer) } } impl From<&BgpSecAsnKey> for ObjectName { fn from(asn_key: &BgpSecAsnKey) -> Self { Self::bgpsec(asn_key.asn, asn_key.key) } } impl From<&str> for ObjectName { fn from(s: &str) -> Self { ObjectName(s.into()) } } impl AsRef for ObjectName { fn as_ref(&self) -> &str { &self.0 } } impl AsRef<[u8]> for ObjectName { fn as_ref(&self) -> &[u8] { self.0.as_bytes() } } impl fmt::Display for ObjectName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) } } //------------ Revocation ---------------------------------------------------- /// Information for an entry on a CRL. // // *Warning:* This type is used in stored state. #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Revocation { /// The serial number of the certificate to be revoked. serial: Serial, /// The revocation date. /// /// This is the "revocationDate" as described in section 5.1 of RFC 5280. /// /// It is set to the time that this object was first created, but it will /// be persisted for future use. There is no support for future or past /// dating this time. #[serde(default = "Time::now")] revocation_date: Time, /// The expiry time of the revoked object. /// /// This is used to determine when a CRL entry can be deleted because it /// is no longer relevant. expires: Time, } impl Revocation { pub fn new(serial: Serial, expires: Time) -> Self { Revocation { serial, revocation_date: Time::now(), expires, } } } impl From<&Cert> for Revocation { fn from(cer: &Cert) -> Self { Revocation::new(cer.serial_number(), cer.validity().not_after()) } } impl From<&Manifest> for Revocation { fn from(m: &Manifest) -> Self { Self::from(m.cert()) } } impl From<&Roa> for Revocation { fn from(r: &Roa) -> Self { Self::from(r.cert()) } } impl From<&Aspa> for Revocation { fn from(aspa: &Aspa) -> Self { Self::from(aspa.cert()) } } //------------ Revocations --------------------------------------------------- /// The list of revocation entries of a CRL. // // *Warning:* This type is used in stored state. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct Revocations(Vec); impl Revocations { /// Converts the revocations to a list of CRL entries. pub fn to_crl_entries(&self) -> Vec { self.0.iter().map(|r| { CrlEntry::new(r.serial, r.revocation_date) }).collect() } /// Removes all expired revocations, and returns them. pub fn remove_expired(&mut self) -> Vec { let (relevant, expired) = self.0.iter().partition(|r| { r.expires > Time::now() }); self.0 = relevant; expired } /// Adds a revociation entry to the list. /// /// The entry is added at the end of the list. pub fn add(&mut self, revocation: Revocation) { self.0.push(revocation); } /// Removes the given revocation entry from the list if present. pub fn remove(&mut self, revocation: &Revocation) { self.0.retain(|existing| existing != revocation); } /// Applies a revocation delta to the list. pub fn apply_delta(&mut self, delta: RevocationsDelta) { self.0.retain(|r| !delta.dropped.contains(r)); for r in delta.added { self.add(r); } } } //------------ RevocationsDelta ---------------------------------------------- /// A change to a revocation list. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct RevocationsDelta { /// The revocation entries to be added. added: Vec, /// The revocation entries to be removed. dropped: Vec, } impl RevocationsDelta { /// Adds a revocation entry to be added to the revocation list. pub fn add(&mut self, revocation: Revocation) { self.added.push(revocation); } /// Adds a revocation entry to be removed from the revocation list. pub fn drop(&mut self, revocation: Revocation) { self.dropped.push(revocation); } } //------------ ResourceSetSummary -------------------------------------------- /// A summary of a set of Internet Number Resources. /// /// This is used for concise reporting. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ResourceSetSummary { /// The number of ASN blocks in the set. pub asn_blocks: usize, /// The number of blocks of IPv4 prefixes in the set. pub ipv4_blocks: usize, /// The number of blocks of IPv6 prefixes in the set. pub ipv6_blocks: usize, } impl From<&ResourceSet> for ResourceSetSummary { fn from(rs: &ResourceSet) -> Self { ResourceSetSummary { asn_blocks: rs.asn().iter().count(), ipv4_blocks: rs.ipv4().iter().count(), ipv6_blocks: rs.ipv6().iter().count(), } } } impl fmt::Display for ResourceSetSummary { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "asn: {} blocks, v4: {} blocks, v6: {} blocks", self.asn_blocks, self.ipv4_blocks, self.ipv6_blocks ) } } //------------ CertAuthList -------------------------------------------------- /// A list of CA summaries. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct CertAuthList { /// The list of CA summaries. /// /// Even though we only have one field, we chose not to use a tuple struct /// here to allow for future extensions more easily. pub cas: Vec, } impl fmt::Display for CertAuthList { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for ca in &self.cas { writeln!(f, "{}", ca.handle)?; } Ok(()) } } //------------ CertAuthSummary ----------------------------------------------- /// The summary of a CA. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct CertAuthSummary { /// The handle identifying the CA. pub handle: CaHandle, } //------------ ParentKindInfo ------------------------------------------------ /// The kind of a parent CA. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum ParentKindInfo { /// The CA is a trust anchor and does not have a parent. Ta, /// The parent is a CA in the same Krill instance. Embedded, /// The parent is a remote CA with communication via RFC 6492. Rfc6492, } impl fmt::Display for ParentKindInfo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ParentKindInfo::Ta => write!(f, "This CA is a TA"), ParentKindInfo::Embedded => write!(f, "Embedded parent"), ParentKindInfo::Rfc6492 => write!(f, "RFC 6492 Parent"), } } } //------------ ParentInfo ---------------------------------------------------- /// Information about a parent of a CA. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ParentInfo { /// The handle identifying the parent CA. pub handle: ParentHandle, /// The kind of parent. pub kind: ParentKindInfo, } impl fmt::Display for ParentInfo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Handle: {} Kind: {}", self.handle, self.kind) } } //------------ ParentStatuses ------------------------------------------------ /// The synchronization status of all parent CAs of a CA. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct ParentStatuses(HashMap); impl ParentStatuses { /// Returns the number of parent CAs. pub fn len(&self) -> usize { self.0.len() } /// Returns whether there are no parents. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Returns the status of the parent with the given handle. pub fn get(&self, parent: &ParentHandle) -> Option<&ParentStatus> { self.0.get(parent) } /// Returns a mutable reference to the status adding the default if missing. pub fn get_or_default_mut( &mut self, parent: &ParentHandle, ) -> &mut ParentStatus { if !self.0.contains_key(parent) { self.0.insert(parent.clone(), ParentStatus::default()); } self.0.get_mut(parent).unwrap() } /// Removes the given parent CA. pub fn remove(&mut self, parent: &ParentHandle) { self.0.remove(parent); } /// Inserts the status for the given parent CA. /// /// Overwrites an existing status if the parent CA is already present. pub fn insert(&mut self, parent: ParentHandle, status: ParentStatus) { self.0.insert(parent, status); } /// Iterates over pairs of parent handles and their status. pub fn iter(&self) -> hash_map::Iter { self.0.iter() } /// Creates a sorted list of parents to be synchronized first. /// /// All parents given in `ca_parents` are considered as well as all /// parents part of `self`. /// /// Parents which have no current synchronization status are first. The /// remaining parents are sorted by their last exchange. Within the same /// minute, parents that had a synchronization failure are sorted first. pub fn sync_candidates( &self, ca_parents: Vec<&ParentHandle>, batch: usize, ) -> Vec { let mut parents = vec![]; // Add any parent for which no current status is known to the // candidate list first. for parent in ca_parents { if !self.0.contains_key(parent) { parents.push(parent.clone()); } } // Then add the ones for which we do have a status, sorted by their // last exchange. let mut parents_by_last_exchange = self.sorted_by_last_exchange(); parents.append(&mut parents_by_last_exchange); // But truncate to the specified batch size parents.truncate(batch); parents } /// Return the parents sorted by last exchange. /// /// The parents without an exchange are sorted first. The remaining /// parents are added with longest ago first. /// /// Uses minute grade granularity and in cases where the exchanges /// happened in the same minute failures take precedence (come before) /// successful exchanges. fn sorted_by_last_exchange(&self) -> Vec { let mut sorted_parents: Vec<(&ParentHandle, &ParentStatus)> = self.iter().collect(); sorted_parents.sort_by(|a, b| { // we can map the 'no last exchange' case to 1970.. let a_last_exchange = a.1.last_exchange.as_ref(); let b_last_exchange = b.1.last_exchange.as_ref(); let a_last_exchange_time = a_last_exchange.map(|e| i64::from(e.timestamp)).unwrap_or(0) / 60; let b_last_exchange_time = b_last_exchange.map(|e| i64::from(e.timestamp)).unwrap_or(0) / 60; if a_last_exchange_time == b_last_exchange_time { // compare success / failure let a_last_exchange_res = a_last_exchange .map(|e| e.result.was_success()) .unwrap_or(false); let b_last_exchange_res = b_last_exchange .map(|e| e.result.was_success()) .unwrap_or(false); a_last_exchange_res.cmp(&b_last_exchange_res) } else { a_last_exchange_time.cmp(&b_last_exchange_time) } }); sorted_parents .into_iter() .map(|(handle, _)| handle) .cloned() .collect() } } impl IntoIterator for ParentStatuses { type Item = (ParentHandle, ParentStatus); type IntoIter = hash_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl<'a> IntoIterator for &'a ParentStatuses { type Item = (&'a ParentHandle, &'a ParentStatus); type IntoIter = hash_map::Iter<'a, ParentHandle, ParentStatus>; fn into_iter(self) -> Self::IntoIter { self.0.iter() } } impl fmt::Display for ParentStatuses { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for (parent, status) in self.0.iter() { writeln!(f, "Parent: {parent}")?; match &status.last_exchange { None => writeln!(f, "Status: connection still pending")?, Some(exchange) => { writeln!(f, "URI: {}", exchange.uri)?; writeln!(f, "Status: {}", exchange.result)?; writeln!( f, "Last contacted: {}", exchange.timestamp.into_rfc3339() )?; if exchange.result.was_success() { write!(f, "Resource Entitlements:")?; } else { write!(f, "LAST KNOWN Resource Entitlements:")?; } if status.classes.is_empty() { writeln!(f, " None")?; } else { writeln!(f, " {}", status.all_resources)?; for class in &status.classes { writeln!( f, " resource class: {}", class.class_name() )?; writeln!( f, " entitled resources: {}", class.resource_set() )?; writeln!( f, " entitled not after: {}", class.not_after().to_rfc3339() )?; let uri = class.signing_cert().url(); let cert = BASE64_ENGINE.encode( class.signing_cert().cert() .to_captured().as_slice() ); writeln!(f, " issuing cert uri: {uri}")?; writeln!( f, " issuing cert PEM:\n\n\ -----BEGIN CERTIFICATE-----\n\ {cert}\n\ -----END CERTIFICATE-----\n\n", )?; writeln!(f, " received certificate(s):")?; for issued in class.issued_certs().iter() { let uri = issued.uri(); let cert = BASE64_ENGINE.encode( issued.cert().to_captured().as_slice() ); writeln!(f, " published at: {uri}")?; writeln!( f, " cert PEM:\n\n\ -----BEGIN CERTIFICATE-----\n\ {cert}\n\ -----END CERTIFICATE-----\n\n" )?; } } } } } } Ok(()) } } //------------ ParentStatus -------------------------------------------------- /// The synchronization status of a parent CA. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct ParentStatus { /// The last synchronization exchange with the parent. /// /// This is `None` if there never was an exchange. pub last_exchange: Option, /// The time of the last successful synchronization exchange. /// /// This is `None` if there never was a successful exchange. pub last_success: Option, /// All resources received from the parent. pub all_resources: ResourceSet, /// The list of resource classes. /// /// The struct changed - we did not record classes in 0.9.5 and below. /// Just default to an empty vec in case this field is missing, and /// ignore the 'entitlements' field that used to be there. This will /// be updated as soon as the CA synchronizes with its parent again. #[serde(default)] pub classes: Vec, } impl ParentStatus { /// Returns the error response in case the last exchange failed. pub fn opt_failure(&self) -> Option { self.last_exchange.as_ref().and_then(|e| e.opt_failure()) } /// Sets the last exchange to the given error response. pub fn set_failure(&mut self, uri: ServiceUri, error: ErrorResponse) { self.last_exchange = Some(ParentExchange { timestamp: Timestamp::now(), uri, result: ExchangeResult::Failure(error), }); } /// Sets the entitlements from the given response. pub fn set_entitlements( &mut self, uri: ServiceUri, entitlements: &ResourceClassListResponse, ) { self.set_last_updated(uri); self.classes.clone_from(entitlements.classes()); let mut all_resources = ResourceSet::default(); for class in &self.classes { all_resources = all_resources.union(class.resource_set()) } self.all_resources = all_resources; } /// Sets the last update to now. pub fn set_last_updated(&mut self, uri: ServiceUri) { let timestamp = Timestamp::now(); self.last_exchange = Some(ParentExchange { timestamp, uri, result: ExchangeResult::Success, }); self.last_success = Some(timestamp); } } //------------ RepoStatus ---------------------------------------------------- /// The repository synchronization status. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct RepoStatus { /// The last synchronization exchange with the repository. /// /// This is `None` if there never was an exchange. pub last_exchange: Option, /// The time of the last successful synchronization exchange. /// /// This is `None` if there never was a successful exchange. pub last_success: Option, /// The list of published objects. pub published: Vec, } impl RepoStatus { /// Returns the error response in case the last exchange failed. pub fn opt_failure(&self) -> Option { self.last_exchange.as_ref().and_then(|e| e.opt_failure()) } /// Sets the last exchange to the given error response. pub fn set_failure(&mut self, uri: ServiceUri, error: ErrorResponse) { let timestamp = Timestamp::now(); self.last_exchange = Some(ParentExchange { timestamp, uri, result: ExchangeResult::Failure(error), }); } /// Updates the published objects from the given delta. pub fn update_published(&mut self, uri: ServiceUri, delta: PublishDelta) { let timestamp = Timestamp::now(); self.last_exchange = Some(ParentExchange { timestamp, uri, result: ExchangeResult::Success, }); for element in delta.into_elements() { match element { PublishDeltaElement::Publish(publish) => { let (_tag, uri, base64) = publish.unpack(); self.published.push(PublishedFile { uri, base64 }); } PublishDeltaElement::Update(update) => { let (_tag, uri, base64, _hash) = update.unpack(); self.published.retain(|el| el.uri != uri); self.published.push(PublishedFile { uri, base64 }); } PublishDeltaElement::Withdraw(withdraw) => { let (_tag, uri, _hash) = withdraw.unpack(); self.published.retain(|el| el.uri != uri); } } } self.last_success = Some(timestamp); } /// Sets the last update to now. pub fn set_last_updated(&mut self, uri: ServiceUri) { let timestamp = Timestamp::now(); self.last_exchange = Some(ParentExchange { timestamp, uri, result: ExchangeResult::Success, }); self.last_success = Some(timestamp); } } impl fmt::Display for RepoStatus { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.last_exchange { None => writeln!(f, "Status: connection still pending")?, Some(exchange) => { Time::now(); writeln!(f, "URI: {}", exchange.uri)?; writeln!(f, "Status: {}", exchange.result)?; writeln!( f, "Last contacted: {}", exchange.timestamp.into_rfc3339() )?; if let Some(success) = self.last_success.as_ref() { writeln!( f, "Last successful contact: {}", success.into_rfc3339() )?; } } } Ok(()) } } //------------ ParentExchange ------------------------------------------------ /// Information about an exchange with a remote server. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ParentExchange { /// The time of the exchange. pub timestamp: Timestamp, /// The service URI of the remote server. pub uri: ServiceUri, /// The result of the exchange. pub result: ExchangeResult, } impl ParentExchange { pub fn opt_failure(&self) -> Option { match &self.result { ExchangeResult::Success => None, ExchangeResult::Failure(error) => Some(error.clone()), } } } //------------ ExchangeResult ------------------------------------------------ /// The result of an exchange with a remote server. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[allow(clippy::large_enum_variant)] pub enum ExchangeResult { /// The exchange was concluded successfully. Success, /// The exchange failed with the given error response. Failure(ErrorResponse), } impl ExchangeResult { /// Returns whether the exchange was a success. pub fn was_success(&self) -> bool { match self { ExchangeResult::Success => true, ExchangeResult::Failure(_) => false, } } } impl fmt::Display for ExchangeResult { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ExchangeResult::Success => write!(f, "success"), ExchangeResult::Failure(e) => write!(f, "failure: {}", e.msg), } } } //------------ ChildrenConnectionStats --------------------------------------- /// The synchronization status of all child CAs. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ChildrenConnectionStats { /// The synchronization status of all child CAs. pub children: Vec, } impl ChildrenConnectionStats { /// Returns a list of all the candidates for suspension. /// /// See [`ChildConnectionStats::is_suspension_candidate`] for details. pub fn suspension_candidates( &self, threshold_seconds: i64, ) -> Vec { self.children .iter() .filter(|child| child.is_suspension_candidate(threshold_seconds)) .map(|child| child.handle.clone()) .collect() } } impl fmt::Display for ChildrenConnectionStats { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if !self.children.is_empty() { writeln!(f, "handle,user_agent,last_exchange,result,state")?; for child in &self.children { match &child.last_exchange { None => { writeln!( f, "{},n/a,never,n/a,{}", child.handle, child.state )?; } Some(exchange) => { let agent = exchange.user_agent.as_deref().unwrap_or(""); writeln!( f, "{},{},{},{},{}", child.handle, agent, exchange.timestamp.into_rfc3339(), exchange.result, child.state )?; } } } } Ok(()) } } //------------ ChildConnectionStats ------------------------------------------ /// The synchronization status of a child CA. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ChildConnectionStats { /// The local handle of the child CA. pub handle: ChildHandle, /// The last synchronization exchange with the child CA. /// /// This is `None` if there never was an exchange. pub last_exchange: Option, /// The status of the child CA. pub state: ChildState, } impl ChildConnectionStats { /// Returns whether the child is considered a candidate for suspension. /// /// The child is considered a candidate for suspension if: /// /// * it is Krill 0.9.2-rc and up as we only know the synchronization /// interval for those servers, /// * the last exchange is longer ago than the specified threshold, and /// * the child is not already suspended. pub fn is_suspension_candidate(&self, threshold_seconds: i64) -> bool { if self.state == ChildState::Suspended { false } else { self.last_exchange.as_ref().map(|exchange| { exchange.is_krill_above_0_9_1() && exchange.more_than_seconds_ago(threshold_seconds) }).unwrap_or(false) } } } //------------ ChildStatus --------------------------------------------------- /// The synchronization status of a child CA. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct ChildStatus { /// The last synchronization exchange with the child CA. /// /// This is `None` if there never was an exchange. pub last_exchange: Option, /// The time of the last successful synchronization exchange. /// /// This is `None` if there never was a successful exchange. pub last_success: Option, /// The time the child CA was suspended. /// /// This is `None` if the child CA isn’t suspended. pub suspended: Option, } impl ChildStatus { pub fn set_success(&mut self, user_agent: Option) { let timestamp = Timestamp::now(); self.last_exchange = Some(ChildExchange { result: ExchangeResult::Success, timestamp, user_agent, }); self.last_success = Some(timestamp); self.suspended = None; } pub fn set_failure( &mut self, user_agent: Option, error_response: ErrorResponse, ) { self.last_exchange = Some(ChildExchange { timestamp: Timestamp::now(), result: ExchangeResult::Failure(error_response), user_agent, }); self.suspended = None; } pub fn set_suspended(&mut self) { self.suspended = Some(Timestamp::now()) } pub fn child_state(&self) -> ChildState { if self.suspended.is_none() { ChildState::Active } else { ChildState::Suspended } } } //------------ ChildExchange ------------------------------------------------- /// A synchronization exchange with a child CA. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ChildExchange { /// The time of the exchange. pub timestamp: Timestamp, /// The result of the exchange. pub result: ExchangeResult, /// The user agent of the child CA’s server. pub user_agent: Option, } impl ChildExchange { /// Returns whether the exchange was longer than the given time ago. pub fn more_than_seconds_ago(&self, seconds: i64) -> bool { self.timestamp < Timestamp::now_minus_seconds(seconds) } /// Returns whether the child used Krill 0.9.2-rc1 or above. pub fn is_krill_above_0_9_1(&self) -> bool { if let Some(agent) = &self.user_agent { // local-child is used by local children, it is extremely // unlikely that they would become suspend candidates in // the real world -- but we have to use these to test the // auto-suspend logic in the high-level "suspend.rs" test if agent == "local-child" { return true; } else if let Some(version) = agent.strip_prefix("krill/") { if let Ok(krill_version) = KrillVersion::from_str(version) { return krill_version > KrillVersion::release(0, 9, 1); } } } false } } //------------ Timestamp ----------------------------------------------------- /// A Unix timestamp with second precision in UTC. #[derive( Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, )] pub struct Timestamp(i64); impl Timestamp { /// Returns a new timestamp from the seconds since the Unix epoch. pub fn new(ts: i64) -> Self { Timestamp(ts) } /// Returns a timestamp for the current time. pub fn now() -> Self { Timestamp(Time::now().timestamp()) } /// Returns a timestamp for the given hours from now. pub fn now_plus_hours(hours: i64) -> Self { Timestamp::now().plus_hours(hours) } /// Returns a timestamp the given number of hours past this timestamp. pub fn plus_hours(self, hours: i64) -> Self { self + Duration::hours(hours) } /// Returns a timestamp for the given hours ago from now. pub fn now_minus_hours(hours: i64) -> Self { Timestamp::now().minus_hours(hours) } /// Returns a timestamp the given number of hours before this timestamp. pub fn minus_hours(self, hours: i64) -> Self { self - Duration::hours(hours) } /// Returns a timestamp for the given minutes from now. pub fn now_plus_minutes(minutes: i64) -> Self { Timestamp::now().plus_minutes(minutes) } /// Returns a timestamp the given number of minutes past this timestamp. pub fn plus_minutes(self, minutes: i64) -> Self { self + Duration::minutes(minutes) } /// Returns a timestamp the given number of seconds before this timestamp. pub fn minus_seconds(self, seconds: i64) -> Self { self - Duration::seconds(seconds) } /// Returns a timestamp the given number of seconds past this timestamp. pub fn plus_seconds(self, seconds: i64) -> Self { self + Duration::seconds(seconds) } /// Returns a timestamp for the given seconds ago from now. pub fn now_minus_seconds(seconds: i64) -> Self { Timestamp::now().minus_seconds(seconds) } /// Returns a timestamp for the given seconds from now. pub fn now_plus_seconds(seconds: i64) -> Self { Timestamp::now().plus_seconds(seconds) } /// Converts the timestamp to a string in RFC 3339 format. pub fn into_rfc3339(self) -> String { Time::from(self).to_rfc3339() } } //--- From impl From for Time { fn from(timestamp: Timestamp) -> Self { Time::new( Utc.timestamp_opt(timestamp.0, 0) .single() .expect("timestamp out-of-range"), ) } } impl From