mirror of
https://github.com/NLnetLabs/krill.git
synced 2026-09-30 21:34:53 +02:00
This PR sticks a warning on all types that are used when serializing Krill’s state. This hopefully will serve as a reminder that they cannot be changed without considering migrations.
413 lines
14 KiB
Rust
413 lines
14 KiB
Rust
//! Managing child CAs.
|
||
|
||
use std::collections::HashMap;
|
||
use rpki::ca::provisioning::ResourceClassName;
|
||
use rpki::crypto::KeyIdentifier;
|
||
use rpki::repository::resources::ResourceSet;
|
||
use serde::{Deserialize, Serialize};
|
||
use crate::api::ca::{
|
||
ChildCaInfo, ChildState, IdCertInfo, IssuedCertificate, ReceivedCert,
|
||
SuspendedCert, UnsuspendedCert,
|
||
};
|
||
use crate::commons::KrillResult;
|
||
use crate::commons::crypto::{KrillSigner, SignSupport};
|
||
use crate::commons::error::Error;
|
||
use crate::config::IssuanceTimingConfig;
|
||
|
||
|
||
//------------ UsedKeyState --------------------------------------------------
|
||
|
||
/// Tracks the state of a key used by a child CA.
|
||
///
|
||
/// This is needed because RFC 6492 dictates that keys cannot be re-used
|
||
/// across resource classes.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
#[allow(clippy::large_enum_variant)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum UsedKeyState {
|
||
/// The key is used by the given resource class.
|
||
///
|
||
/// Multiple keys are possible during a key rollover.
|
||
#[serde(alias = "current")]
|
||
InUse(ResourceClassName),
|
||
|
||
/// The key has been revoked.
|
||
Revoked,
|
||
}
|
||
|
||
|
||
//------------ ChildDetails --------------------------------------------------
|
||
|
||
/// Information about a child CA needed by a parent CA.
|
||
///
|
||
/// Note that the actual [`IssuedCertificate`] corresponding to the
|
||
/// [`KeyIdentifier`] and [`ResourceClassName`] are kept in the parent's
|
||
/// resource class.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||
pub struct ChildDetails {
|
||
/// The state of the child.
|
||
#[serde(default)]
|
||
pub state: ChildState,
|
||
|
||
/// The ID certificate to communicate with the child CA.
|
||
pub id_cert: IdCertInfo,
|
||
|
||
/// The resources the child CA is entitled to.
|
||
pub resources: ResourceSet,
|
||
|
||
/// The set of keys used by the child.
|
||
pub used_keys: HashMap<KeyIdentifier, UsedKeyState>,
|
||
|
||
/// Mapping of the resource class names used by the child to the parents.
|
||
///
|
||
/// Parent and child usually use the same name, but
|
||
/// we need this mapping in case a delegated child CA was exported
|
||
/// somewhere and then imported into Krill. In such cases the
|
||
/// resource class names that were used for the child may not match
|
||
/// the internal resource class names used. See issue: 1133
|
||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||
pub rcn_map: HashMap<ResourceClassName, ResourceClassName>,
|
||
}
|
||
|
||
impl ChildDetails {
|
||
/// Creates the child details from the ID certificate and resources.
|
||
///
|
||
/// Sets the state to active and both the used keys and RCN map to empty.
|
||
pub fn new(id_cert: IdCertInfo, resources: ResourceSet) -> Self {
|
||
ChildDetails {
|
||
state: ChildState::Active,
|
||
id_cert,
|
||
resources,
|
||
used_keys: HashMap::new(),
|
||
rcn_map: HashMap::new(),
|
||
}
|
||
}
|
||
|
||
/// Returns the child info for this child.
|
||
pub fn to_info(&self) -> ChildCaInfo {
|
||
ChildCaInfo {
|
||
state: self.state,
|
||
id_cert: self.id_cert.clone(),
|
||
entitled_resources: self.resources.clone(),
|
||
}
|
||
}
|
||
|
||
/// Convert the internal resource class name to that used by the parent.
|
||
///
|
||
/// The method takes the resource class name used internally and returns
|
||
/// the name to be used when talking to the parent. If the internal name
|
||
/// is part of the `rcn_map`, this name is used. Otherwise the internal
|
||
/// name is also the parent name.
|
||
pub fn name_for_parent_rcn(
|
||
&self, name_in_parent: &ResourceClassName,
|
||
) -> ResourceClassName {
|
||
self.rcn_map.get(name_in_parent).unwrap_or(name_in_parent).clone()
|
||
}
|
||
|
||
/// Convert the parent’s resource class name to that used internally.
|
||
///
|
||
/// The method thakes the resource class name as used by the parent and
|
||
/// returns the name we use internally. If the parent name is part of the
|
||
/// `rcn_map`, this name is used. Otherwise the parent name is also the
|
||
/// internal name.
|
||
pub fn parent_name_for_rcn(
|
||
&self,
|
||
name_in_child: &ResourceClassName,
|
||
) -> ResourceClassName {
|
||
self.rcn_map.iter()
|
||
.find(|(_k, v)| *v == name_in_child)
|
||
.map(|(k, _v)| k.clone())
|
||
.unwrap_or_else(|| name_in_child.clone())
|
||
}
|
||
|
||
/// Returns all keys that ae issued for the given parent resource class.
|
||
pub fn issued(
|
||
&self,
|
||
parent_rcn: &ResourceClassName,
|
||
) -> Vec<KeyIdentifier> {
|
||
let mut res = vec![];
|
||
|
||
for (ki, used_key_state) in self.used_keys.iter() {
|
||
if let UsedKeyState::InUse(found_rcn) = used_key_state {
|
||
if found_rcn == parent_rcn {
|
||
res.push(*ki)
|
||
}
|
||
}
|
||
}
|
||
|
||
res
|
||
}
|
||
|
||
/// Returns whether the given key is currently issued.
|
||
pub fn is_issued(&self, ki: &KeyIdentifier) -> bool {
|
||
matches!(self.used_keys.get(ki), Some(UsedKeyState::InUse(_)))
|
||
}
|
||
|
||
/// Returns an error in case the key is already in use in another class.
|
||
pub fn verify_key_allowed(
|
||
&self,
|
||
ki: &KeyIdentifier,
|
||
parent_rcn: &ResourceClassName,
|
||
) -> KrillResult<()> {
|
||
if let Some(last_response) = self.used_keys.get(ki) {
|
||
let allowed = match last_response {
|
||
UsedKeyState::Revoked => false,
|
||
UsedKeyState::InUse(found) => found == parent_rcn,
|
||
};
|
||
if !allowed {
|
||
return Err(Error::KeyUseAttemptReuse);
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
|
||
//------------ ChildCertificates -------------------------------------------
|
||
|
||
/// The collection of certificates issued under a resource class.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||
pub struct ChildCertificates {
|
||
/// The certificates for active CAs.
|
||
//
|
||
// Note: we cannot remove the alias unless we migrate existing json on
|
||
// upgrade.
|
||
#[serde(alias = "inner")]
|
||
issued: HashMap<KeyIdentifier, IssuedCertificate>,
|
||
|
||
/// The certificates for suspeneded child CAs.
|
||
#[serde(
|
||
skip_serializing_if = "HashMap::is_empty",
|
||
default = "HashMap::new"
|
||
)]
|
||
suspended: HashMap<KeyIdentifier, SuspendedCert>,
|
||
}
|
||
|
||
impl ChildCertificates {
|
||
/// Returns whether the set of keys is empty.
|
||
pub fn is_empty(&self) -> bool {
|
||
self.issued.is_empty() && self.suspended.is_empty()
|
||
}
|
||
|
||
/// Adds a new issued certificate.
|
||
pub fn add_issued_certificate(&mut self, issued: IssuedCertificate) {
|
||
let ki = issued.key_identifier();
|
||
self.issued.insert(ki, issued);
|
||
}
|
||
|
||
/// Unsuspends a certificate and move it to the issued certificates.
|
||
pub fn unsuspend_certificate(&mut self, unsuspended: UnsuspendedCert) {
|
||
let ki = unsuspended.key_identifier();
|
||
self.suspended.remove(&ki);
|
||
self.issued.insert(ki, unsuspended.into_converted());
|
||
}
|
||
|
||
/// Suspends a certificate and moves it to the suspended certificates.
|
||
pub fn suspend_certificate(&mut self, suspended: SuspendedCert) {
|
||
let ki = suspended.key_identifier();
|
||
self.issued.remove(&ki);
|
||
self.suspended.insert(ki, suspended);
|
||
}
|
||
|
||
/// Removes the certificate for a revoke a key.
|
||
pub fn remove_revoked_key(&mut self, key: &KeyIdentifier) {
|
||
self.issued.remove(key);
|
||
self.suspended.remove(key);
|
||
}
|
||
|
||
/// Returns the issued certificate for the given key if available.
|
||
pub fn get_issued(
|
||
&self,
|
||
ki: &KeyIdentifier,
|
||
) -> Option<&IssuedCertificate> {
|
||
self.issued.get(ki)
|
||
}
|
||
|
||
/// Returns the suspended certificate for the given key if available.
|
||
pub fn get_suspended(
|
||
&self,
|
||
ki: &KeyIdentifier,
|
||
) -> Option<&SuspendedCert> {
|
||
self.suspended.get(ki)
|
||
}
|
||
|
||
/// Re-issue everything when activating a new key
|
||
pub fn activate_key(
|
||
&self,
|
||
signing_cert: &ReceivedCert,
|
||
issuance_timing: &IssuanceTimingConfig,
|
||
signer: &KrillSigner,
|
||
) -> KrillResult<ChildCertificateUpdates> {
|
||
let mut updates = ChildCertificateUpdates::default();
|
||
for issued in self.issued.values() {
|
||
updates.issued.push(self.re_issue(
|
||
issued,
|
||
None,
|
||
signing_cert,
|
||
issuance_timing,
|
||
signer,
|
||
)?);
|
||
}
|
||
// Also re-issue suspended certificates, they may yet become
|
||
// unsuspended at some point
|
||
for suspended in self.suspended.values() {
|
||
updates.suspended.push(
|
||
self.re_issue(
|
||
&suspended.to_converted(),
|
||
None,
|
||
signing_cert,
|
||
issuance_timing,
|
||
signer,
|
||
)?
|
||
.into_converted(),
|
||
);
|
||
}
|
||
Ok(updates)
|
||
}
|
||
|
||
/// Shrink any overclaiming certificates.
|
||
///
|
||
/// Note: We need to pro-actively shrink child certificates to avoid
|
||
/// invalidating them. But, if we gain additional resources it is up to
|
||
/// child to request a new certificate with those resources.
|
||
pub fn shrink_overclaiming(
|
||
&self,
|
||
received_cert: &ReceivedCert,
|
||
issuance_timing: &IssuanceTimingConfig,
|
||
signer: &KrillSigner,
|
||
) -> KrillResult<ChildCertificateUpdates> {
|
||
let mut updates = ChildCertificateUpdates::default();
|
||
|
||
let updated_resources = &received_cert.resources;
|
||
|
||
for issued in self.issued.values() {
|
||
if let Some(reduced_set) =
|
||
issued.reduced_applicable_resources(updated_resources)
|
||
{
|
||
if reduced_set.is_empty() {
|
||
// revoke
|
||
updates.removed.push(issued.key_identifier());
|
||
}
|
||
else {
|
||
// re-issue
|
||
updates.issued.push(self.re_issue(
|
||
issued,
|
||
Some(reduced_set),
|
||
received_cert,
|
||
issuance_timing,
|
||
signer,
|
||
)?);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Also shrink suspended, in case they would come back
|
||
for suspended in self.suspended.values() {
|
||
if let Some(reduced_set) =
|
||
suspended.reduced_applicable_resources(updated_resources)
|
||
{
|
||
if reduced_set.is_empty() {
|
||
// revoke
|
||
updates.removed.push(suspended.key_identifier());
|
||
}
|
||
else {
|
||
// re-issue shrunk suspended
|
||
//
|
||
// Note: this will not be published yet, but remain
|
||
// suspended until the child contacts us again, or is
|
||
// manually un-suspended.
|
||
updates.suspended.push(
|
||
self.re_issue(
|
||
&suspended.to_converted(),
|
||
Some(reduced_set),
|
||
received_cert,
|
||
issuance_timing,
|
||
signer,
|
||
)?.into_converted(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(updates)
|
||
}
|
||
|
||
/// Return a re-issued certficate for an issued certificate.
|
||
///
|
||
/// The returned certificate should be used to replace the previous
|
||
/// certificat if it has become outdated or has changed resources.
|
||
fn re_issue(
|
||
&self,
|
||
previous: &IssuedCertificate,
|
||
updated_resources: Option<ResourceSet>,
|
||
signing_cert: &ReceivedCert,
|
||
issuance_timing: &IssuanceTimingConfig,
|
||
signer: &KrillSigner,
|
||
) -> KrillResult<IssuedCertificate> {
|
||
let csr_info = previous.csr_info.clone();
|
||
let resource_set = updated_resources.unwrap_or_else(|| {
|
||
previous.resources.clone()
|
||
});
|
||
let limit = previous.limit.clone();
|
||
|
||
let re_issued = SignSupport::make_issued_cert(
|
||
csr_info,
|
||
&resource_set,
|
||
limit,
|
||
signing_cert,
|
||
issuance_timing.new_child_cert_validity(),
|
||
signer,
|
||
)?;
|
||
|
||
Ok(re_issued)
|
||
}
|
||
}
|
||
|
||
|
||
//------------ ChildCertificateUpdates -------------------------------------
|
||
|
||
/// Describes an update to the set of ROAs under a ResourceClass.
|
||
//
|
||
// *Warning:* This type is used in stored state.
|
||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||
pub struct ChildCertificateUpdates {
|
||
/// Issued certificates that have been added.
|
||
///
|
||
/// Note that these are typically newly issued certificates, but can
|
||
/// also be a previously issued certificates which have been suspended
|
||
/// and are now unsuspended.
|
||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||
pub issued: Vec<IssuedCertificate>,
|
||
|
||
/// Key identifiers of certificates that have been removed.
|
||
///
|
||
/// Added keys will be revoked.
|
||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||
pub removed: Vec<KeyIdentifier>,
|
||
|
||
/// The certificates that have been suspended.
|
||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||
pub suspended: Vec<SuspendedCert>,
|
||
|
||
/// The certificats that have been unsuspended.
|
||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||
pub unsuspended: Vec<UnsuspendedCert>,
|
||
}
|
||
|
||
impl ChildCertificateUpdates {
|
||
/// Returns whether there are no updates.
|
||
pub fn is_empty(&self) -> bool {
|
||
self.issued.is_empty()
|
||
&& self.removed.is_empty()
|
||
&& self.suspended.is_empty()
|
||
&& self.unsuspended.is_empty()
|
||
}
|
||
}
|
||
|