use std::fmt;
use std::collections::HashMap;
use rpki::uri;
use rpki::ca::idcert::IdCert;
use rpki::ca::idexchange::{CaHandle, ChildHandle, ParentHandle};
use rpki::ca::provisioning::ResourceClassName;
use rpki::repository::resources::ResourceSet;
use serde::{Deserialize, Deserializer, Serialize};
use crate::commons::KrillResult;
use crate::commons::crypto::CsrInfo;
use crate::commons::error::Error;
use crate::commons::ext_serde::OneOrMany;
use crate::constants::ta_handle;
use super::admin::PublicationServerUris;
use super::roa::RoaConfiguration;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Structure {
#[serde(skip_serializing_if = "Option::is_none")]
pub ta: Option<ImportTa>,
#[serde(skip_serializing_if = "Option::is_none")]
pub publication_server: Option<PublicationServerUris>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
pub cas: Vec<ImportCa>,
}
impl Structure {
pub fn for_testbed(
ta_aia: uri::Rsync,
ta_uri: uri::Https,
publication_server_uris: PublicationServerUris,
cas: Vec<ImportCa>,
) -> Self {
Structure {
ta: Some(ImportTa {
ta_aia,
ta_uri,
ta_key_pem: None,
ta_mft_nr_override: None,
}),
publication_server: Some(publication_server_uris),
cas,
}
}
pub fn validate_ca_hierarchy(
&self,
mut existing_cas: HashMap<ParentHandle, ResourceSet>,
) -> KrillResult<()> {
let ta_handle = ta_handle();
existing_cas.insert(ta_handle.convert(), ResourceSet::all());
for ca in &self.cas {
if ca.handle == ta_handle {
return Err(Error::Custom(format!(
"CA name {ta_handle} is reserved."
)));
}
if existing_cas.contains_key(&ca.handle.convert()) {
return Err(Error::Custom(format!(
"CA with name {} already exists. \
Check import and server state!",
ca.handle
)));
}
let mut ca_resources = ResourceSet::empty();
for ca_parent in &ca.parents {
if let Some(seen_parent_resources) = existing_cas.get(
&ca_parent.handle
) {
if seen_parent_resources.contains(&ca_parent.resources) {
ca_resources = ca_resources.union(
&ca_parent.resources
);
}
else {
return Err(Error::Custom(format!(
"CA '{}' under parent '{}' claims resources not \
held by parent.",
ca.handle,
ca_parent.handle
)));
}
}
else {
return Err(Error::Custom(format!(
"CA '{}' wants parent '{}', but this parent CA does \
not appear before this CA.",
ca.handle,
ca_parent.handle
)));
}
}
existing_cas.insert(ca.handle.convert(), ca_resources);
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ImportTa {
pub ta_aia: uri::Rsync,
pub ta_uri: uri::Https,
#[serde(skip_serializing_if = "Option::is_none")]
pub ta_key_pem: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ta_mft_nr_override: Option<u64>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ImportCa {
pub handle: CaHandle,
#[serde(rename = "parent", deserialize_with = "deserialize_parent")]
pub parents: Vec<ImportParent>,
#[serde(default = "Vec::new")]
pub roas: Vec<RoaConfiguration>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ImportParent {
pub handle: ParentHandle,
pub resources: ResourceSet,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ImportChild {
pub name: ChildHandle,
pub id_cert: IdCert,
pub resources: ResourceSet,
pub issued_cert: ImportChildCertificate,
}
#[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<ChildResourceClassName>,
}
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()
)?;
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(())
}
}
pub type ChildResourceClassName = ResourceClassName;
fn deserialize_parent<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<ImportParent>, D::Error> {
OneOrMany::<ImportParent>::deserialize(deserializer)
.map(|oom| oom.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_cas_only() {
let json = include_str!(
"../../test-resources/bulk-ca-import/structure.json"
);
let structure: Structure = serde_json::from_str(json).unwrap();
assert!(structure.validate_ca_hierarchy(HashMap::new()).is_ok());
}
#[test]
fn parse_import_delegated_child() {
let json = include_str!(
"../../test-resources/bulk-ca-import/import-nicbr.json"
);
let _child: ImportChild = serde_json::from_str(json).unwrap();
}
}