diff --git a/src/cli/client.rs b/src/cli/client.rs index 5b1adf11..2249bbd5 100644 --- a/src/cli/client.rs +++ b/src/cli/client.rs @@ -281,6 +281,16 @@ impl KrillClient { delete(&self.server, &self.token, &uri).await?; Ok(ApiResponse::Empty) } + CaCommand::ChildExport(handle, child) => { + let uri = format!("api/v1/cas/{}/children/{}/export", handle, child); + let response = get_json(&self.server, &self.token, &uri).await?; + Ok(ApiResponse::ChildExported(response)) + } + CaCommand::ChildImport(handle, child) => { + let uri = format!("api/v1/cas/{}/children/{}/import", handle, child.name); + post_json(&self.server, &self.token, &uri, child).await?; + Ok(ApiResponse::Empty) + } CaCommand::ChildConnections(handle) => { let uri = format!("api/v1/cas/{}/stats/children/connections", handle); let stats: ChildrenConnectionStats = get_json(&self.server, &self.token, &uri).await?; @@ -547,7 +557,10 @@ impl KrillClient { ); if let Some(storage_uri) = details.data_dir() { - config = config.replace("### storage_uri = \"./data\"", &format!("storage_uri = \"{}\"", storage_uri)) + config = config.replace( + "### storage_uri = \"./data\"", + &format!("storage_uri = \"{}\"", storage_uri), + ) } if let Some(log_file) = details.log_file() { diff --git a/src/cli/options.rs b/src/cli/options.rs index c2a57617..c9938e6f 100644 --- a/src/cli/options.rs +++ b/src/cli/options.rs @@ -30,10 +30,10 @@ use crate::{ cli::report::{ReportError, ReportFormat}, commons::{ api::{ - self, AddChildRequest, AspaCustomer, AspaDefinition, AspaDefinitionFormatError, AspaProvidersUpdate, - AuthorizationFmtError, BgpSecAsnKey, BgpSecDefinition, CertAuthInit, ParentCaReq, PublicationServerUris, - RepoFileDeleteCriteria, RoaConfiguration, RoaConfigurationUpdates, RoaPayload, RtaName, Token, - UpdateChildRequest, + self, import::ImportChild, AddChildRequest, AspaCustomer, AspaDefinition, AspaDefinitionFormatError, + AspaProvidersUpdate, AuthorizationFmtError, BgpSecAsnKey, BgpSecDefinition, CertAuthInit, ParentCaReq, + PublicationServerUris, RepoFileDeleteCriteria, RoaConfiguration, RoaConfigurationUpdates, RoaPayload, + RtaName, Token, UpdateChildRequest, }, crypto::SignSupport, error::KrillIoError, @@ -2542,6 +2542,8 @@ pub enum CaCommand { ChildAdd(CaHandle, AddChildRequest), ChildUpdate(CaHandle, ChildHandle, UpdateChildRequest), ChildDelete(CaHandle, ChildHandle), + ChildExport(CaHandle, ChildHandle), + ChildImport(CaHandle, ImportChild), ChildConnections(CaHandle), // Key Management diff --git a/src/cli/report.rs b/src/cli/report.rs index 98ff3055..f2a7ef47 100644 --- a/src/cli/report.rs +++ b/src/cli/report.rs @@ -8,10 +8,10 @@ use rpki::ca::idexchange; use crate::{ commons::{ api::{ - AllCertAuthIssues, AspaDefinitionList, BgpSecCsrInfoList, CaCommandDetails, CaRepoDetails, CertAuthInfo, - CertAuthIssues, CertAuthList, ChildCaInfo, ChildrenConnectionStats, CommandHistory, ConfiguredRoas, - IdCertInfo, ParentCaContact, ParentStatuses, PublisherDetails, PublisherList, RepoStatus, - RepositoryContact, RtaList, RtaPrepResponse, ServerInfo, + import::ExportChild, AllCertAuthIssues, AspaDefinitionList, BgpSecCsrInfoList, CaCommandDetails, + CaRepoDetails, CertAuthInfo, CertAuthIssues, CertAuthList, ChildCaInfo, ChildrenConnectionStats, + CommandHistory, ConfiguredRoas, IdCertInfo, ParentCaContact, ParentStatuses, PublisherDetails, + PublisherList, RepoStatus, RepositoryContact, RtaList, RtaPrepResponse, ServerInfo, }, bgp::{BgpAnalysisAdvice, BgpAnalysisReport, BgpAnalysisSuggestion}, }, @@ -50,6 +50,7 @@ pub enum ApiResponse { ParentStatuses(ParentStatuses), ChildInfo(ChildCaInfo), + ChildExported(ExportChild), ChildrenStats(ChildrenConnectionStats), PublisherDetails(PublisherDetails), @@ -98,6 +99,7 @@ impl ApiResponse { ApiResponse::ParentCaContact(contact) => Ok(Some(contact.report(fmt)?)), ApiResponse::ParentStatuses(statuses) => Ok(Some(statuses.report(fmt)?)), ApiResponse::ChildInfo(info) => Ok(Some(info.report(fmt)?)), + ApiResponse::ChildExported(child) => Ok(Some(child.report(fmt)?)), ApiResponse::ChildrenStats(stats) => Ok(Some(stats.report(fmt)?)), ApiResponse::PublisherList(list) => Ok(Some(list.report(fmt)?)), ApiResponse::PublisherDetails(details) => Ok(Some(details.report(fmt)?)), @@ -187,6 +189,7 @@ impl Report for IdCertInfo {} impl Report for RepositoryContact {} impl Report for ChildCaInfo {} +impl Report for ExportChild {} impl Report for ParentCaContact {} impl Report for ParentStatuses {} diff --git a/src/commons/api/admin.rs b/src/commons/api/admin.rs index 5de3510a..fbc67632 100644 --- a/src/commons/api/admin.rs +++ b/src/commons/api/admin.rs @@ -9,6 +9,7 @@ use rpki::{ idcert::IdCert, idexchange::{self, ServiceUri}, idexchange::{CaHandle, ChildHandle, ParentHandle, PublisherHandle, RepoInfo}, + provisioning::ResourceClassName, }, crypto::PublicKey, repository::resources::ResourceSet, @@ -546,6 +547,15 @@ pub struct UpdateChildRequest { #[serde(skip_serializing_if = "Option::is_none")] suspend: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + resource_class_name_mapping: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ResourceClassNameMapping { + pub name_in_parent: ResourceClassName, + pub name_for_child: ResourceClassName, } impl UpdateChildRequest { @@ -554,6 +564,7 @@ impl UpdateChildRequest { id_cert, resources, suspend, + resource_class_name_mapping: None, } } pub fn id_cert(id_cert: IdCert) -> Self { @@ -561,6 +572,7 @@ impl UpdateChildRequest { id_cert: Some(id_cert), resources: None, suspend: None, + resource_class_name_mapping: None, } } @@ -569,6 +581,7 @@ impl UpdateChildRequest { id_cert: None, resources: Some(resources), suspend: None, + resource_class_name_mapping: None, } } @@ -577,6 +590,7 @@ impl UpdateChildRequest { id_cert: None, resources: None, suspend: Some(true), + resource_class_name_mapping: None, } } @@ -585,11 +599,33 @@ impl UpdateChildRequest { id_cert: None, resources: None, suspend: Some(false), + resource_class_name_mapping: None, } } - pub fn unpack(self) -> (Option, Option, Option) { - (self.id_cert, self.resources, self.suspend) + pub fn resource_class_name_mapping(mapping: ResourceClassNameMapping) -> Self { + UpdateChildRequest { + id_cert: None, + resources: None, + suspend: None, + resource_class_name_mapping: Some(mapping), + } + } + + pub fn unpack( + self, + ) -> ( + Option, + Option, + Option, + Option, + ) { + ( + self.id_cert, + self.resources, + self.suspend, + self.resource_class_name_mapping, + ) } } diff --git a/src/commons/api/history.rs b/src/commons/api/history.rs index 8805254c..e29f1e58 100644 --- a/src/commons/api/history.rs +++ b/src/commons/api/history.rs @@ -23,7 +23,7 @@ use crate::{ daemon::ca::{CertAuth, DropReason}, }; -use super::{AspaDefinitionUpdates, ResourceSetSummary}; +use super::{AspaDefinitionUpdates, ResourceClassNameMapping, ResourceSetSummary}; //------------ CommandHistory ------------------------------------------------ @@ -367,6 +367,11 @@ pub enum CertAuthStorableCommand { ski: String, resources: ResourceSet, }, + ChildImport { + child: ChildHandle, + ski: String, + resources: ResourceSet, + }, ChildUpdateResources { child: ChildHandle, resources: ResourceSet, @@ -375,6 +380,10 @@ pub enum CertAuthStorableCommand { child: ChildHandle, ski: String, }, + ChildUpdateResourceClassNameMapping { + child: ChildHandle, + mapping: ResourceClassNameMapping, + }, ChildCertify { child: ChildHandle, resource_class_name: ResourceClassName, @@ -474,6 +483,12 @@ impl WithStorableDetails for CertAuthStorableCommand { .with_id_ski(ski.as_ref()) .with_resources(resources) } + CertAuthStorableCommand::ChildImport { child, ski, resources } => { + CommandSummary::new("cmd-ca-child-import", self) + .with_child(child) + .with_id_ski(ski) + .with_resources(resources) + } CertAuthStorableCommand::ChildUpdateResources { child, resources } => { CommandSummary::new("cmd-ca-child-update-res", self) .with_child(child) @@ -484,6 +499,12 @@ impl WithStorableDetails for CertAuthStorableCommand { .with_child(child) .with_id_ski(ski) } + CertAuthStorableCommand::ChildUpdateResourceClassNameMapping { child, mapping } => { + CommandSummary::new("cmd-ca-child-update-rcn-mapping", self) + .with_child(child) + .with_arg("parent_rcn", &mapping.name_in_parent) + .with_arg("child_rcn", &mapping.name_for_child) + } CertAuthStorableCommand::ChildCertify { child, resource_class_name, @@ -614,6 +635,14 @@ impl fmt::Display for CertAuthStorableCommand { child, ski, summary ) } + CertAuthStorableCommand::ChildImport { child, ski, resources } => { + let summary = ResourceSetSummary::from(resources); + write!( + f, + "Import child '{}' with RFC8183 key '{}' and resources '{}'", + child, ski, summary + ) + } CertAuthStorableCommand::ChildUpdateResources { child, resources } => { let summary = ResourceSetSummary::from(resources); write!(f, "Update resources for child '{}' to: {}", child, summary) @@ -621,6 +650,13 @@ impl fmt::Display for CertAuthStorableCommand { CertAuthStorableCommand::ChildUpdateId { child, ski } => { write!(f, "Update child '{}' RFC 8183 key '{}'", child, ski) } + CertAuthStorableCommand::ChildUpdateResourceClassNameMapping { child, mapping } => { + write!( + f, + "Update child '{}' map parent RC '{}' to '{}' for child", + child, mapping.name_in_parent, mapping.name_for_child + ) + } CertAuthStorableCommand::ChildCertify { child, ki, .. } => { write!(f, "Issue certificate to child '{}' for key '{}'", child, ki) } diff --git a/src/commons/api/import.rs b/src/commons/api/import.rs index 9eacc132..9e8ed048 100644 --- a/src/commons/api/import.rs +++ b/src/commons/api/import.rs @@ -1,23 +1,29 @@ //! Data types used to support importing a CA structure for testing or automated set ups. -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; use serde::{Deserialize, Deserializer}; use rpki::{ - ca::idexchange::{CaHandle, ParentHandle}, + ca::{ + idcert::IdCert, + idexchange::{CaHandle, ChildHandle, ParentHandle}, + provisioning::ResourceClassName, + }, repository::resources::ResourceSet, uri, }; use crate::{ - commons::{api::PublicationServerUris, error::Error, KrillResult}, + commons::{api::PublicationServerUris, crypto::CsrInfo, error::Error, KrillResult}, daemon::config, ta::ta_handle, }; use super::RoaConfiguration; +//------------ Structure ----------------------------------------------------- + /// This type contains the full structure of CAs and signed objects etc that is /// set up when the import API is used. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -30,19 +36,6 @@ pub struct Structure { pub cas: Vec, } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct ImportTa { - pub ta_aia: uri::Rsync, - pub ta_uri: uri::Https, - pub ta_key_pem: Option, -} - -impl ImportTa { - pub fn unpack(self) -> (uri::Rsync, Vec, Option) { - (self.ta_aia, vec![self.ta_uri], self.ta_key_pem) - } -} - impl Structure { pub fn new( ta_aia: uri::Rsync, @@ -129,6 +122,23 @@ where config::OneOrMany::::deserialize(deserializer).map(|oom| oom.into()) } +//------------ ImportTa ------------------------------------------------------ + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ImportTa { + pub ta_aia: uri::Rsync, + pub ta_uri: uri::Https, + pub ta_key_pem: Option, +} + +impl ImportTa { + pub fn unpack(self) -> (uri::Rsync, Vec, Option) { + (self.ta_aia, vec![self.ta_uri], self.ta_key_pem) + } +} + +//------------ ImportCa ------------------------------------------------------ + /// This type describes a CaStructure that needs to be imported. I.e. it describes /// a CA at the top of a branch and recursively includes 0 or more children of this /// same type. @@ -155,6 +165,8 @@ impl ImportCa { } } +//------------ ImportParent -------------------------------------------------- + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ImportParent { handle: ParentHandle, @@ -175,6 +187,59 @@ impl ImportParent { } } +//------------ ImportChild --------------------------------------------------- + +pub type ExportChild = ImportChild; + +/// Describes a child CA that can be imported from, or exported to, +/// another parent CA instance. +/// +/// Only supports the simplest scenario where the child has only +/// one certificate, in only one resource class. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ImportChild { + pub name: ChildHandle, + pub id_cert: IdCert, + pub resources: ResourceSet, + pub issued_cert: ImportChildCertificate, +} + +pub type ChildResourceClassName = ResourceClassName; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ImportChildCertificate { + #[serde(flatten)] + pub csr: CsrInfo, + #[serde(skip_serializing_if = "Option::is_none")] + pub class_name: Option, +} + +impl fmt::Display for ImportChild { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Name: {}", self.name)?; + writeln!( + f, + "Id Key: {}", + self.id_cert.public_key().key_identifier().to_string() + )?; + writeln!(f, "Resources: {}", self.resources)?; + if let Some(class_name) = &self.issued_cert.class_name { + writeln!(f, "Classname: {}", class_name)?; + } + let (ca_repository, rpki_manifest, rpki_notify, key) = self.issued_cert.csr.clone().unpack(); + + writeln!(f, "Issued Certificate:")?; + writeln!(f, " Key Id: {}", key.key_identifier())?; + writeln!(f, " CA repo: {}", ca_repository)?; + writeln!(f, " CA mft: {}", rpki_manifest)?; + if let Some(rrdp) = rpki_notify { + writeln!(f, " RRDP: {}", rrdp)?; + } + + Ok(()) + } +} + #[cfg(test)] mod tests { diff --git a/src/daemon/ca/certauth.rs b/src/daemon/ca/certauth.rs index e8ba2dca..9b42e787 100644 --- a/src/daemon/ca/certauth.rs +++ b/src/daemon/ca/certauth.rs @@ -1,4 +1,10 @@ -use std::{collections::HashMap, convert::TryFrom, ops::Deref, sync::Arc, vec}; +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + ops::Deref, + sync::Arc, + vec, +}; use bytes::Bytes; use chrono::Duration; @@ -25,10 +31,11 @@ use rpki::{ use crate::{ commons::{ api::{ + import::{ExportChild, ImportChild, ImportChildCertificate}, AspaCustomer, AspaDefinition, AspaDefinitionList, AspaDefinitionUpdates, AspaProvidersUpdate, BgpSecAsnKey, BgpSecCsrInfoList, BgpSecDefinitionUpdates, CertAuthInfo, CertAuthStorableCommand, ConfiguredRoa, - IdCertInfo, IssuedCertificate, ObjectName, ParentCaContact, ReceivedCert, RepositoryContact, Revocation, - RoaConfiguration, RoaConfigurationUpdates, RtaList, RtaName, RtaPrepResponse, + IdCertInfo, ObjectName, ParentCaContact, ReceivedCert, RepositoryContact, ResourceClassNameMapping, + Revocation, RoaConfiguration, RoaConfigurationUpdates, RtaList, RtaName, RtaPrepResponse, }, crypto::{CsrInfo, KrillSigner}, error::{Error, RoaDeltaError}, @@ -215,6 +222,16 @@ impl Aggregate for CertAuth { self.children.get_mut(&child).unwrap().set_resources(resources) } + CertAuthEvent::ChildUpdatedResourceClassNameMapping { + child, + name_in_parent, + name_for_child, + } => self + .children + .get_mut(&child) + .unwrap() + .add_mapping(name_in_parent, name_for_child), + CertAuthEvent::ChildRemoved { child } => { self.children.remove(&child); } @@ -417,10 +434,16 @@ impl Aggregate for CertAuth { match command.into_details() { // being a parent CertAuthCommandDetails::ChildAdd(child, id_cert, resources) => self.child_add(child, id_cert, resources), + CertAuthCommandDetails::ChildImport(import_child, config, signer) => { + self.child_import(import_child, &config, signer) + } CertAuthCommandDetails::ChildUpdateResources(child, res) => self.child_update_resources(&child, res), CertAuthCommandDetails::ChildUpdateId(child, id_cert) => self.child_update_id_cert(&child, id_cert), + CertAuthCommandDetails::ChildUpdateResourceClassNameMapping(child, mapping) => { + self.child_resource_class_name_mapping(child, mapping) + } CertAuthCommandDetails::ChildCertify(child, request, config, signer) => { - self.child_certify(child, request, &config, signer) + self.child_certify_from_command(child, request, &config, signer) } CertAuthCommandDetails::ChildRevokeKey(child, request) => self.child_revoke_key(child, request), CertAuthCommandDetails::ChildRemove(child) => self.child_remove(&child), @@ -596,6 +619,55 @@ impl CertAuth { /// # Being a parent /// impl CertAuth { + /// Export a child under this CA, if possible. + pub fn child_export(&self, child_handle: &ChildHandle) -> KrillResult { + let child = self.get_child(child_handle)?; + + let id_cert = child.id_cert().try_into()?; + let resources = child.resources().clone(); + + if self.resources.len() != 1 { + return Err(Error::custom( + "export child is not supported for multiple resource classes.", + )); + } + let (my_rcn, rc) = self.resources.iter().next().unwrap(); // there is exactly 1 entry + + let issued_key = { + let issued_keys = child.issued(my_rcn); + if issued_keys.len() != 1 { + return Err(Error::custom( + "export child is not supported if child has no issued certificate, or is doing a key rollover.", + )); + } + issued_keys[0] + }; + + let issued_cert = rc + .issued(&issued_key) + .ok_or(Error::custom("no issued certificate found for child to export"))?; + + let csr = issued_cert.csr_info().clone(); + + let class_name = { + let child_rcn = child.name_for_parent_rcn(my_rcn); + if my_rcn != &child_rcn { + Some(child_rcn) + } else { + None + } + }; + + let issued_cert = ImportChildCertificate { csr, class_name }; + + Ok(ExportChild { + name: child_handle.clone(), + id_cert, + resources, + issued_cert, + }) + } + pub fn verify_rfc6492(&self, cms: ProvisioningCms) -> KrillResult { let child_handle = cms.message().sender().convert(); let child = self.get_child(&child_handle).map_err(|e| { @@ -635,8 +707,8 @@ impl CertAuth { ) -> KrillResult { let mut classes = vec![]; - for rcn in self.resources.keys() { - if let Some(class) = self.entitlement_class(child_handle, rcn, issuance_timing)? { + for my_rcn in self.resources.keys() { + if let Some(class) = self.entitlement_class(child_handle, my_rcn, issuance_timing)? { classes.push(class); } } @@ -649,12 +721,12 @@ impl CertAuth { pub fn issuance_response( &self, child_handle: &ChildHandle, - class_name: &ResourceClassName, + my_rcn: &ResourceClassName, pub_key: &PublicKey, issuance_timing: &IssuanceTimingConfig, ) -> KrillResult { let entitlement_class = self - .entitlement_class(child_handle, class_name, issuance_timing)? + .entitlement_class(child_handle, my_rcn, issuance_timing)? .ok_or(Error::KeyUseNoIssuedCert)?; entitlement_class @@ -666,10 +738,10 @@ impl CertAuth { fn entitlement_class( &self, child_handle: &ChildHandle, - rcn: &ResourceClassName, + my_rcn: &ResourceClassName, issuance_timing: &IssuanceTimingConfig, ) -> KrillResult> { - let my_rc = match self.resources.get(rcn) { + let my_rc = match self.resources.get(my_rcn) { Some(rc) => rc, None => return Ok(None), }; @@ -700,7 +772,7 @@ impl CertAuth { return Ok(None); } - let child_keys = child.issued(rcn); + let child_keys = child.issued(my_rcn); let mut issued_certs = vec![]; @@ -739,8 +811,10 @@ impl CertAuth { } } + let child_rcn = child.name_for_parent_rcn(my_rcn); + Ok(Some(ResourceClassEntitlements::new( - rcn.clone(), + child_rcn, child_resources, not_after, issued_certs, @@ -785,65 +859,132 @@ impl CertAuth { } } + /// Import a child (from another CA) and adopt it as our own. + fn child_import( + &self, + import_child: ImportChild, + config: &Config, + signer: Arc, + ) -> KrillResult> { + // overview: + // - perform checks (e.g. not supported in case we have multiple RCs) + // - add the child + // - add the resource class mapping if given + // - sign a new certificate for the child + // Combine all events and return them. + + let (child_handle, id_cert, resources, issued_cert) = ( + import_child.name, + import_child.id_cert, + import_child.resources, + import_child.issued_cert, + ); + let id_cert_info = IdCertInfo::from(id_cert); + + let (class_name_override, csr_info) = (issued_cert.class_name, issued_cert.csr); + let limit = RequestResourceLimit::default(); // i.e. no limit + + // Ensure that we have one, and only one, resource class + // and get its name. + let my_rcn = if self.resources.len() != 1 { + Err(Error::custom( + "cannot import CA unless parent has exactly one resource class", + )) + } else { + self.resources + .keys() + .next() + .ok_or(Error::custom("cannot get resource class")) + }? + .clone(); + + let mut events = vec![]; + + // Add the child + events.append(&mut self.child_add(child_handle.clone(), id_cert_info, resources.clone())?); + + // Add a resource class name mapping if applicable + if let Some(name_for_child) = class_name_override { + if name_for_child != my_rcn { + let mapping = ResourceClassNameMapping { + name_in_parent: my_rcn.clone(), + name_for_child, + }; + + events.push(CertAuthEvent::child_updated_resource_class_name_mapping( + child_handle.clone(), + mapping, + )); + } + } + + // Issue a certificate for the imported child + events.append(&mut self.child_certify(child_handle, &resources, my_rcn, csr_info, limit, config, signer)?); + + Ok(events) + } + /// Certifies a child, unless: /// = the child is unknown, /// = the child is not authorized, /// = the csr is invalid, /// = the limit exceeds the child allocation, /// = the signer throws up.. - fn child_certify( + fn child_certify_from_command( &self, - child: ChildHandle, + child_handle: ChildHandle, request: IssuanceRequest, config: &Config, signer: Arc, ) -> KrillResult> { - let (rcn, limit, csr) = request.unpack(); + let (child_rcn, limit, csr) = request.unpack(); + + let child = self.get_child(&child_handle)?; + let my_rcn = child.parent_name_for_rcn(&child_rcn); let csr_info = CsrInfo::try_from(&csr)?; + self.child_certify(child_handle, child.resources(), my_rcn, csr_info, limit, config, signer) + } + + fn child_certify( + &self, + child_handle: ChildHandle, + resources: &ResourceSet, + my_rcn: ResourceClassName, + csr_info: CsrInfo, + limit: RequestResourceLimit, + config: &Config, + signer: Arc, + ) -> KrillResult> { if !csr_info.global_uris() && !test_mode_enabled() { return Err(Error::invalid_csr( "MUST use hostnames in URIs for certificate requests.", )); } - let issued = - self.issue_child_certificate(&child, rcn.clone(), csr_info, limit, &config.issuance_timing, &signer)?; + let my_rc = self + .resources + .get(&my_rcn) + .ok_or_else(|| Error::ResourceClassUnknown(my_rcn.clone()))?; + let issued = my_rc.issue_cert(csr_info, resources, limit, &config.issuance_timing, &signer)?; let cert_name = ObjectName::new(&issued.key_identifier(), "cer"); info!( "CA '{}' issued certificate '{}' to child '{}'", - self.handle, cert_name, child + self.handle, cert_name, child_handle ); - let issued_event = CertAuthEvent::child_certificate_issued(child, rcn.clone(), issued.key_identifier()); + let issued_event = + CertAuthEvent::child_certificate_issued(child_handle, my_rcn.clone(), issued.key_identifier()); let mut cert_updates = ChildCertificateUpdates::default(); cert_updates.issue(issued); - let child_certs_updated = CertAuthEvent::child_certificates_updated(rcn, cert_updates); + let child_certs_updated = CertAuthEvent::child_certificates_updated(my_rcn, cert_updates); Ok(vec![issued_event, child_certs_updated]) } - /// Issue a new child certificate. - fn issue_child_certificate( - &self, - child: &ChildHandle, - rcn: ResourceClassName, - csr_info: CsrInfo, - limit: RequestResourceLimit, - issuance_timing: &IssuanceTimingConfig, - signer: &KrillSigner, - ) -> KrillResult { - let my_rc = self.resources.get(&rcn).ok_or(Error::ResourceClassUnknown(rcn))?; - let child = self.get_child(child)?; - - // note this will ultimately return an error if the requested limit exceeds - // the child's resources. - my_rc.issue_cert(csr_info, child.resources(), limit, issuance_timing, signer) - } - /// Updates child Resource entitlements. /// /// This does not yet revoke / reissue / republish anything. @@ -906,6 +1047,32 @@ impl CertAuth { } } + /// Updates the child resource class name mapping + fn child_resource_class_name_mapping( + &self, + child_handle: ChildHandle, + mapping: ResourceClassNameMapping, + ) -> KrillResult> { + // fails if the child is unknown. + let child = self.get_child(&child_handle)?; + + if !self.resources.contains_key(&mapping.name_in_parent) { + warn!("About to update resource class name mapping for child '{}, but parent does not have any resource class called '{}', or at least not yet.", child_handle, &mapping.name_in_parent); + } + + if !child.issued(&mapping.name_in_parent).is_empty() { + return Err(Error::Custom(format!( + "Cannot add mapping for RC '{}', child already received certificate(s).", + mapping.name_in_parent + ))); + } + + Ok(vec![CertAuthEvent::child_updated_resource_class_name_mapping( + child_handle, + mapping, + )]) + } + /// Revokes a key for a child. So, add the last cert for the key to the CRL, and withdraw /// the .cer file for it. fn child_revoke_key( @@ -913,9 +1080,10 @@ impl CertAuth { child_handle: ChildHandle, request: RevocationRequest, ) -> KrillResult> { - let (rcn, key) = request.unpack(); + let (child_rcn, key) = request.unpack(); let child = self.get_child(&child_handle)?; + let my_rcn = child.parent_name_for_rcn(&child_rcn); if !child.is_issued(&key) { return Err(Error::KeyUseNoIssuedCert); @@ -930,8 +1098,8 @@ impl CertAuth { self.handle, cert_name, child_handle ); - let rev = CertAuthEvent::child_revoke_key(child_handle, rcn.clone(), key); - let upd = CertAuthEvent::child_certificates_updated(rcn, child_certificate_updates); + let rev = CertAuthEvent::child_revoke_key(child_handle, my_rcn.clone(), key); + let upd = CertAuthEvent::child_certificates_updated(my_rcn, child_certificate_updates); Ok(vec![rev, upd]) } diff --git a/src/daemon/ca/child.rs b/src/daemon/ca/child.rs index 207fc558..5604e44d 100644 --- a/src/daemon/ca/child.rs +++ b/src/daemon/ca/child.rs @@ -24,7 +24,7 @@ use crate::{ #[allow(clippy::large_enum_variant)] #[serde(rename_all = "snake_case")] pub enum UsedKeyState { - Current(ResourceClassName), + InUse(ResourceClassName), // Multiple keys are possible during a key rollover. Revoked, } @@ -41,6 +41,8 @@ pub struct ChildDetails { id_cert: IdCertInfo, resources: ResourceSet, used_keys: HashMap, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + rcn_map: HashMap, } impl ChildDetails { @@ -50,6 +52,7 @@ impl ChildDetails { id_cert, resources, used_keys: HashMap::new(), + rcn_map: HashMap::new(), } } @@ -81,12 +84,44 @@ impl ChildDetails { self.resources = resources; } - pub fn issued(&self, rcn: &ResourceClassName) -> Vec { + pub fn add_mapping(&mut self, name_in_parent: ResourceClassName, name_for_child: ResourceClassName) { + self.rcn_map.insert(name_in_parent, name_for_child); + } + + /// Resolve the resource class name used by the child, to the + /// internal name used by its parent. + /// + /// Note that the parent and child usually use the same name, but + /// we need this mapping in case a delegated child CA was exported + /// from 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 + pub(super) fn name_for_parent_rcn(&self, name_in_parent: &ResourceClassName) -> ResourceClassName { + self.rcn_map.get(name_in_parent).unwrap_or(name_in_parent).clone() + } + + /// Resolve the resource class name used by the parent, to the + /// name used by the child in request and responses. + /// + /// Note that the parent and child usually use the same name, but + /// we need this mapping in case a delegated child CA was exported + /// from 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 + pub(super) 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()) + } + + pub fn issued(&self, parent_rcn: &ResourceClassName) -> Vec { let mut res = vec![]; for (ki, used_key_state) in self.used_keys.iter() { - if let UsedKeyState::Current(found_rcn) = used_key_state { - if found_rcn == rcn { + if let UsedKeyState::InUse(found_rcn) = used_key_state { + if found_rcn == parent_rcn { res.push(*ki) } } @@ -96,11 +131,11 @@ impl ChildDetails { } pub fn is_issued(&self, ki: &KeyIdentifier) -> bool { - matches!(self.used_keys.get(ki), Some(UsedKeyState::Current(_))) + matches!(self.used_keys.get(ki), Some(UsedKeyState::InUse(_))) } - pub fn add_issue_response(&mut self, rcn: ResourceClassName, ki: KeyIdentifier) { - self.used_keys.insert(ki, UsedKeyState::Current(rcn)); + pub fn add_issue_response(&mut self, parent_rcn: ResourceClassName, ki: KeyIdentifier) { + self.used_keys.insert(ki, UsedKeyState::InUse(parent_rcn)); } pub fn add_revoke_response(&mut self, ki: KeyIdentifier) { @@ -108,11 +143,11 @@ impl ChildDetails { } /// Returns an error in case the key is already in use in another class. - pub fn verify_key_allowed(&self, ki: &KeyIdentifier, rcn: &ResourceClassName) -> KrillResult<()> { + 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::Current(found) => found == rcn, + UsedKeyState::InUse(found) => found == parent_rcn, }; if !allowed { return Err(Error::KeyUseAttemptReuse); diff --git a/src/daemon/ca/commands.rs b/src/daemon/ca/commands.rs index 92f76baa..33202ab6 100644 --- a/src/daemon/ca/commands.rs +++ b/src/daemon/ca/commands.rs @@ -17,9 +17,9 @@ use crate::{ commons::{ actor::Actor, api::{ - AspaCustomer, AspaDefinitionUpdates, AspaProvidersUpdate, BgpSecDefinitionUpdates, CertAuthStorableCommand, - IdCertInfo, ParentCaContact, ReceivedCert, RepositoryContact, RoaConfigurationUpdates, RtaName, - StorableRcEntitlement, + import::ImportChild, AspaCustomer, AspaDefinitionUpdates, AspaProvidersUpdate, BgpSecDefinitionUpdates, + CertAuthStorableCommand, IdCertInfo, ParentCaContact, ReceivedCert, RepositoryContact, + ResourceClassNameMapping, RoaConfigurationUpdates, RtaName, StorableRcEntitlement, }, crypto::KrillSigner, eventsourcing::{self, InitCommandDetails, SentCommand, SentInitCommand, WithStorableDetails}, @@ -83,6 +83,9 @@ pub enum CertAuthCommandDetails { // Add a new child under this parent CA ChildAdd(ChildHandle, IdCertInfo, ResourceSet), + // Import a child under this parent CA + ChildImport(ImportChild, Arc, Arc), + // Update the resource entitlements for an existing child. ChildUpdateResources(ChildHandle, ResourceSet), @@ -90,6 +93,10 @@ pub enum CertAuthCommandDetails { // provisioning protocol. ChildUpdateId(ChildHandle, IdCertInfo), + // Update the mapping the parent uses to map its own resource + // class name to another name for the child. + ChildUpdateResourceClassNameMapping(ChildHandle, ResourceClassNameMapping), + // Process an issuance request sent by an existing child. ChildCertify(ChildHandle, IssuanceRequest, Arc, Arc), @@ -258,6 +265,11 @@ impl From for CertAuthStorableCommand { ski: id_cert.public_key().key_identifier().to_string(), resources, }, + CertAuthCommandDetails::ChildImport(import_child, _, _) => CertAuthStorableCommand::ChildImport { + child: import_child.name, + ski: import_child.id_cert.public_key().key_identifier().to_string(), + resources: import_child.resources, + }, CertAuthCommandDetails::ChildUpdateResources(child, resources) => { CertAuthStorableCommand::ChildUpdateResources { child, resources } } @@ -265,6 +277,9 @@ impl From for CertAuthStorableCommand { child, ski: id_cert.public_key().key_identifier().to_string(), }, + CertAuthCommandDetails::ChildUpdateResourceClassNameMapping(child, mapping) => { + CertAuthStorableCommand::ChildUpdateResourceClassNameMapping { child, mapping } + } CertAuthCommandDetails::ChildCertify(child, req, _, _) => { let (resource_class_name, limit, csr) = req.unpack(); let ki = csr.public_key().key_identifier(); @@ -397,6 +412,21 @@ impl CertAuthCommandDetails { ) } + pub fn child_import( + handle: &CaHandle, + child: ImportChild, + config: Arc, + signer: Arc, + actor: &Actor, + ) -> CertAuthCommand { + eventsourcing::SentCommand::new( + handle, + None, + CertAuthCommandDetails::ChildImport(child, config, signer), + actor, + ) + } + pub fn child_update_resources( handle: &CaHandle, child_handle: ChildHandle, @@ -425,6 +455,20 @@ impl CertAuthCommandDetails { ) } + pub fn child_update_resource_class_name_mapping( + handle: &CaHandle, + child_handle: ChildHandle, + mapping: ResourceClassNameMapping, + actor: &Actor, + ) -> CertAuthCommand { + eventsourcing::SentCommand::new( + handle, + None, + CertAuthCommandDetails::ChildUpdateResourceClassNameMapping(child_handle, mapping), + actor, + ) + } + /// Certify a child. Will return an error in case the child is /// unknown, or in case resources are not held by the child. pub fn child_certify( diff --git a/src/daemon/ca/events.rs b/src/daemon/ca/events.rs index 73649f15..02754710 100644 --- a/src/daemon/ca/events.rs +++ b/src/daemon/ca/events.rs @@ -13,7 +13,8 @@ use crate::{ commons::{ api::{ AspaCustomer, AspaDefinition, AspaProvidersUpdate, BgpSecAsnKey, IdCertInfo, IssuedCertificate, ObjectName, - ParentCaContact, ReceivedCert, RepositoryContact, RoaAggregateKey, RtaName, SuspendedCert, UnsuspendedCert, + ParentCaContact, ReceivedCert, RepositoryContact, ResourceClassNameMapping, RoaAggregateKey, RtaName, + SuspendedCert, UnsuspendedCert, }, crypto::KrillSigner, eventsourcing::{Event, InitEvent}, @@ -460,6 +461,11 @@ pub enum CertAuthEvent { child: ChildHandle, resources: ResourceSet, }, + ChildUpdatedResourceClassNameMapping { + child: ChildHandle, + name_in_parent: ResourceClassName, + name_for_child: ResourceClassName, + }, ChildRemoved { child: ChildHandle, }, @@ -704,7 +710,6 @@ impl CertAuthEvent { updates, } } - pub(super) fn child_removed(child: ChildHandle) -> CertAuthEvent { CertAuthEvent::ChildRemoved { child } } @@ -716,6 +721,17 @@ impl CertAuthEvent { pub(super) fn child_unsuspended(child: ChildHandle) -> CertAuthEvent { CertAuthEvent::ChildUnsuspended { child } } + + pub(super) fn child_updated_resource_class_name_mapping( + child: ChildHandle, + mapping: ResourceClassNameMapping, + ) -> CertAuthEvent { + CertAuthEvent::ChildUpdatedResourceClassNameMapping { + child, + name_in_parent: mapping.name_in_parent, + name_for_child: mapping.name_for_child, + } + } } impl fmt::Display for CertAuthEvent { @@ -804,6 +820,18 @@ impl fmt::Display for CertAuthEvent { CertAuthEvent::ChildUpdatedResources { child, resources } => { write!(f, "updated child '{}' resources to '{}'", child, resources) } + CertAuthEvent::ChildUpdatedResourceClassNameMapping { + child, + name_in_parent, + name_for_child, + } => { + write!( + f, + "updated child '{}' map parent RC name '{}' to '{}' for child", + child, name_in_parent, name_for_child + ) + } + CertAuthEvent::ChildRemoved { child } => write!(f, "removed child '{}'", child), CertAuthEvent::ChildSuspended { child } => write!(f, "suspended child '{}'", child), CertAuthEvent::ChildUnsuspended { child } => write!(f, "unsuspended child '{}'", child), diff --git a/src/daemon/ca/manager.rs b/src/daemon/ca/manager.rs index f2c390b3..110d1758 100644 --- a/src/daemon/ca/manager.rs +++ b/src/daemon/ca/manager.rs @@ -24,8 +24,10 @@ use crate::{ commons::{ actor::Actor, api::{ - rrdp::PublishElement, BgpSecCsrInfoList, BgpSecDefinitionUpdates, IdCertInfo, ParentServerInfo, - PublicationServerInfo, RoaConfigurationUpdates, Timestamp, + import::{ExportChild, ImportChild}, + rrdp::PublishElement, + BgpSecCsrInfoList, BgpSecDefinitionUpdates, IdCertInfo, ParentServerInfo, PublicationServerInfo, + RoaConfigurationUpdates, Timestamp, }, api::{ AddChildRequest, AspaCustomer, AspaDefinitionList, AspaDefinitionUpdates, AspaProvidersUpdate, @@ -650,6 +652,35 @@ impl CaManager { ca.get_child(child).map(|details| details.clone().into()) } + /// Export a child. Fails if: + /// - the child does not exist + /// - the child has no received certificate + /// - the child has more than one received certificate or resource class + /// + /// Primarily meant for testing that the child import function works. + pub async fn ca_child_export(&self, ca: &CaHandle, child_handle: &ChildHandle) -> KrillResult { + trace!("Exporting CA: {} under parent: {}", child_handle, ca); + self.get_ca(ca).await?.child_export(child_handle) + } + + /// Import a child under the given CA. Will fail if: + /// - the ca does not exist + /// - the ca has less than, or more than one resource class + /// - the ca does not hold the resources for the child + /// - the child already exists + pub async fn ca_child_import(&self, ca: &CaHandle, import_child: ImportChild, actor: &Actor) -> KrillResult<()> { + trace!("Importing CA: {} under parent: {}", import_child.name, ca); + self.send_ca_command(CertAuthCommandDetails::child_import( + ca, + import_child, + self.config.clone(), + self.signer.clone(), + actor, + )) + .await?; + Ok(()) + } + /// Show a contact for a child. pub async fn ca_parent_contact( &self, @@ -710,7 +741,7 @@ impl CaManager { req: UpdateChildRequest, actor: &Actor, ) -> KrillResult<()> { - let (id_opt, resources_opt, suspend_opt) = req.unpack(); + let (id_opt, resources_opt, suspend_opt, resource_class_name_mapping_opt) = req.unpack(); if let Some(id) = id_opt { self.send_ca_command(CertAuthCommandDetails::child_update_id( @@ -732,13 +763,19 @@ impl CaManager { } if let Some(suspend) = suspend_opt { if suspend { - self.send_ca_command(CertAuthCommandDetails::child_suspend_inactive(ca, child, actor)) + self.send_ca_command(CertAuthCommandDetails::child_suspend_inactive(ca, child.clone(), actor)) .await?; } else { - self.send_ca_command(CertAuthCommandDetails::child_unsuspend(ca, child, actor)) + self.send_ca_command(CertAuthCommandDetails::child_unsuspend(ca, child.clone(), actor)) .await?; } } + if let Some(mapping) = resource_class_name_mapping_opt { + self.send_ca_command(CertAuthCommandDetails::child_update_resource_class_name_mapping( + ca, child, mapping, actor, + )) + .await?; + } Ok(()) } @@ -895,20 +932,21 @@ impl CaManager { async fn issue( &self, ca_handle: &CaHandle, - child: ChildHandle, + child_handle: ChildHandle, issue_req: IssuanceRequest, actor: &Actor, ) -> KrillResult { if ca_handle.as_str() == TA_NAME { let request = ta::ProvisioningRequest::Issuance(issue_req); - self.ta_slow_rfc6492_request(ca_handle, child, request, actor).await + self.ta_slow_rfc6492_request(ca_handle, child_handle, request, actor) + .await } else { - let class_name = issue_req.class_name(); + let child_rcn = issue_req.class_name(); let pub_key = issue_req.csr().public_key(); let cmd = CertAuthCommandDetails::child_certify( ca_handle, - child.clone(), + child_handle.clone(), issue_req.clone(), self.config.clone(), self.signer.clone(), @@ -918,11 +956,14 @@ impl CaManager { let ca = self.send_ca_command(cmd).await?; // The updated CA will now include the newly issued certificate. - let response = ca.issuance_response(&child, class_name, pub_key, &self.config.issuance_timing)?; + let child = ca.get_child(&child_handle)?; + let my_rcn = child.parent_name_for_rcn(child_rcn); + + let response = ca.issuance_response(&child_handle, &my_rcn, pub_key, &self.config.issuance_timing)?; Ok(provisioning::Message::issue_response( ca_handle.convert(), - child.into_converted(), + child_handle.into_converted(), response, )) } diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index 7ca7aab7..6556d7dd 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -1440,6 +1440,26 @@ async fn api_ca_child_show(req: Request, ca: CaHandle, child: ChildHandle) -> Ro ) } +async fn api_ca_child_export(req: Request, ca: CaHandle, child: ChildHandle) -> RoutingResult { + aa!( + req, + Permission::CA_READ, + Handle::from(&ca), + render_json_res(req.state().api_ca_child_export(&ca, &child).await) + ) +} + +async fn api_ca_child_import(req: Request, ca: CaHandle) -> RoutingResult { + aa!(req, Permission::CA_ADMIN, Handle::from(&ca), { + let actor = req.actor(); + let server = req.state().clone(); + match req.json().await { + Ok(import_child) => render_empty_res(server.api_ca_child_import(&ca, import_child, &actor).await), + Err(e) => render_error(e), + } + }) +} + async fn api_ca_stats_child_connections(req: Request, ca: CaHandle) -> RoutingResult { aa!( req, @@ -1648,6 +1668,8 @@ async fn api_ca_children(req: Request, path: &mut RequestPath, ca: CaHandle) -> }, Some("contact") | Some("parent_response.json") => api_ca_parent_res_json(req, ca, child).await, Some("parent_response.xml") => api_ca_parent_res_xml(req, ca, child).await, + Some("export") => api_ca_child_export(req, ca, child).await, + Some("import") => api_ca_child_import(req, ca).await, _ => render_unknown_method(), }, None => match *req.method() { diff --git a/src/daemon/krillserver.rs b/src/daemon/krillserver.rs index cc467d44..2c13854e 100644 --- a/src/daemon/krillserver.rs +++ b/src/daemon/krillserver.rs @@ -19,7 +19,9 @@ use crate::{ commons::{ actor::{Actor, ActorDef}, api::{ - self, AddChildRequest, AllCertAuthIssues, AspaCustomer, AspaDefinitionList, AspaDefinitionUpdates, + self, + import::{ExportChild, ImportChild}, + AddChildRequest, AllCertAuthIssues, AspaCustomer, AspaDefinitionList, AspaDefinitionUpdates, AspaProvidersUpdate, BgpSecCsrInfoList, BgpSecDefinitionUpdates, CaCommandDetails, CaRepoDetails, CertAuthInfo, CertAuthInit, CertAuthIssues, CertAuthList, CertAuthStats, ChildCaInfo, ChildrenConnectionStats, CommandHistory, CommandHistoryCriteria, ConfiguredRoa, IdCertInfo, @@ -482,8 +484,17 @@ impl KrillServer { /// Show details for a child under the CA. pub async fn ca_child_show(&self, ca: &CaHandle, child: &ChildHandle) -> KrillResult { - let child = self.ca_manager.ca_show_child(ca, child).await?; - Ok(child) + self.ca_manager.ca_show_child(ca, child).await + } + + /// Export a child under the CA. + pub async fn api_ca_child_export(&self, ca: &CaHandle, child: &ChildHandle) -> KrillResult { + self.ca_manager.ca_child_export(ca, child).await + } + + /// Import a child under the CA. + pub async fn api_ca_child_import(&self, ca: &CaHandle, child: ImportChild, actor: &Actor) -> KrillResult<()> { + self.ca_manager.ca_child_import(ca, child, actor).await } /// Show children stats under the CA. diff --git a/src/daemon/mq.rs b/src/daemon/mq.rs index 011ce8a8..dbfae967 100644 --- a/src/daemon/mq.rs +++ b/src/daemon/mq.rs @@ -366,7 +366,7 @@ impl TaskQueue { now(), ), - CertAuthEvent::ParentAdded { parent, .. } => { + CertAuthEvent::ParentAdded { parent, .. } | CertAuthEvent::ParentUpdated { parent, .. } => { if ca.repository_contact().is_ok() { debug!("Parent {} added to CA {}, scheduling sync", parent, ca_handle); self.schedule( diff --git a/src/ta/proxy.rs b/src/ta/proxy.rs index 828667e3..d177bddb 100644 --- a/src/ta/proxy.rs +++ b/src/ta/proxy.rs @@ -434,7 +434,7 @@ impl eventsourcing::Aggregate for TrustAnchorProxy { ProvisioningResponse::Issuance(_) => { child_details .used_keys - .insert(key_id, UsedKeyState::Current("default".into())); + .insert(key_id, UsedKeyState::InUse("default".into())); } ProvisioningResponse::Revocation(_) => { child_details.used_keys.insert(key_id, UsedKeyState::Revoked); diff --git a/tests/functional_delegated_ca_import.rs b/tests/functional_delegated_ca_import.rs new file mode 100644 index 00000000..54570fdf --- /dev/null +++ b/tests/functional_delegated_ca_import.rs @@ -0,0 +1,166 @@ +//! Test export and import of a delegated CA child from +//! a parent in one Krill instance into a parent in another +//! Krill instance. + +use krill::{ + cli::{ + options::{CaCommand, Command}, + report::ApiResponse, + }, + commons::api::{ + import::{ExportChild, ImportChild}, + ParentCaReq, ResourceClassNameMapping, UpdateChildRequest, + }, + test::*, +}; +use rpki::{ + ca::{ + idexchange::{CaHandle, ParentResponse}, + provisioning::ResourceClassName, + }, + repository::resources::ResourceSet, +}; + +#[tokio::test] +async fn functional_delegated_ca_import() { + async fn start_testbed(port: u16) -> impl FnOnce() { + let (data_dir, cleanup) = tmp_dir(); + let storage_uri = mem_storage(); + let mut config = test_config(&storage_uri, Some(&data_dir), true, true, false, false); + config.port = port; + start_krill(config).await; + + cleanup + } + + // Start a testbed + // Start a second testbed + // Add child in testbed one + // - add child + // - override default resource class name for child + // - add parent to child + // Export the child in testbed one + // Import the child in testbed two + // Update the child to use testbed two + // Update the child resources + + let testbed = ca_handle("testbed"); + let parent_1 = ca_handle("parent_1"); + let parent_2 = ca_handle("parent_2"); + + let parent_res = ResourceSet::all(); + + let child = ca_handle("child"); + let child_res = resources("AS65000", "10.0.0.0/16", ""); + let child_res_2 = resources("AS65000-AS65010", "10.0.0.0/8", "2001:db8::/32"); + let child_rcn = ResourceClassName::from("custom"); + + // Start a testbed + let clean = start_testbed(3000).await; + + // Add parent_1 + set_up_ca_with_repo(&parent_1).await; + set_up_ca_under_parent(&parent_1, &testbed, &parent_res, None).await; + + // Add child under parent_1 + set_up_ca_with_repo(&child).await; + set_up_ca_under_parent(&child, &parent_1, &child_res, Some(child_rcn)).await; + + // Export the child + let exported_child = export_child(&parent_1, &child).await; + + // Add parent_2 + set_up_ca_with_repo(&parent_2).await; + set_up_ca_under_parent(&parent_2, &testbed, &parent_res, None).await; + + // Import child into parent_2 + import_child(&parent_2, exported_child).await; + + // Add testbed in other server as parent to child + let response = parent_contact(&parent_2, &child).await; + let parent_ca_req = ParentCaReq::new(parent_2.convert(), response); + add_parent_to_ca(&child, parent_ca_req).await; + + // Remove the child from the original parent + delete_child(&parent_1, &child).await; + + // Update the resources for the child in the new + // parent, then synchronise it, and verify that + // the resources are received. + update_child_resources(&parent_2, &child, &child_res_2).await; + assert!(ca_contains_resources(&child, &child_res_2).await); + + clean(); +} + +async fn export_child(parent: &CaHandle, child: &CaHandle) -> ExportChild { + match krill_admin(Command::CertAuth(CaCommand::ChildExport( + parent.clone(), + child.convert(), + ))) + .await + { + ApiResponse::ChildExported(child) => child, + _ => { + panic!("Expected exported child") + } + } +} + +async fn import_child(parent: &CaHandle, child: ImportChild) { + match krill_admin(Command::CertAuth(CaCommand::ChildImport(parent.clone(), child))).await { + ApiResponse::Empty => {} + _ => { + panic!("Expected exported child") + } + } +} + +async fn set_up_ca_under_parent( + ca: &CaHandle, + parent: &CaHandle, + resources: &ResourceSet, + child_rcn: Option, +) { + let child_request = request(ca).await; + let parent_ca_req = { + let response = add_child_rfc6492(parent.convert(), ca.convert(), child_request, resources.clone()).await; + ParentCaReq::new(parent.convert(), response) + }; + + if let Some(child_rcn) = child_rcn { + let mapping = ResourceClassNameMapping { + name_in_parent: rcn(0), + name_for_child: child_rcn, + }; + krill_admin(krill::cli::options::Command::CertAuth(CaCommand::ChildUpdate( + parent.convert(), + ca.convert(), + UpdateChildRequest::resource_class_name_mapping(mapping), + ))) + .await; + } + add_parent_to_ca(ca, parent_ca_req).await; + assert!(ca_contains_resources(ca, resources).await); +} + +async fn parent_contact(ca: &CaHandle, child: &CaHandle) -> ParentResponse { + match krill_admin(Command::CertAuth(CaCommand::ParentResponse( + ca.clone(), + child.convert(), + ))) + .await + { + ApiResponse::Rfc8183ParentResponse(response) => response, + _ => panic!("Expected RFC 8183 Parent Response"), + } +} + +async fn update_child_resources(ca: &CaHandle, child: &CaHandle, resources: &ResourceSet) { + let child_handle = child.convert(); + let req = UpdateChildRequest::resources(resources.clone()); + match krill_admin(Command::CertAuth(CaCommand::ChildUpdate(ca.clone(), child_handle, req))).await { + ApiResponse::Empty => {} + _ => panic!("Expected empty ok response"), + } +}