use std::fmt;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::str::FromStr;
use log::debug;
use rpki::ca::idexchange::CaHandle;
use rpki::repository::resources::ResourceSet;
use rpki::repository::roa::{Roa, RoaBuilder};
use rpki::repository::sigobj::SignedObjectBuilder;
use rpki::repository::x509::{Time, Validity};
use serde::de;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::api::ca::ObjectName;
use crate::api::roa::{
AsNumber, RoaConfiguration, RoaConfigurationUpdates, RoaInfo, RoaPayload,
RoaPayloadJsonMapKey,
};
use crate::commons::KrillResult;
use crate::commons::crypto::KrillSigner;
use crate::commons::error::{Error, RoaDeltaError};
use crate::config::{Config, IssuanceTimingConfig};
use super::events::CertAuthEvent;
use super::keys::CertifiedKey;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct Routes {
map: HashMap<RoaPayloadJsonMapKey, RouteInfo>,
}
impl Routes {
pub fn filter(&self, resources: &ResourceSet) -> Self {
Self {
map: self.map.iter().flat_map(|(auth, info)| {
if resources.contains_roa_address(
&auth.as_ref().as_roa_ip_address()
) {
Some((*auth, info.clone()))
} else {
None
}
}).collect()
}
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn has(&self, auth: &RoaPayloadJsonMapKey) -> bool {
self.map.contains_key(auth)
}
pub fn get(&self, auth: &RoaPayloadJsonMapKey) -> Option<&RouteInfo> {
self.map.get(auth)
}
pub fn roa_configurations(&self) -> Vec<RoaConfiguration> {
self.map.iter().map(|(payload_key, route_info)| {
RoaConfiguration {
payload: (*payload_key).into(),
comment: route_info.comment.clone(),
}
}).collect()
}
pub fn roa_payload_keys(
&self,
) -> impl Iterator<Item = RoaPayloadJsonMapKey> + '_ {
self.map.keys().copied()
}
fn to_aggregates(
&self,
) -> HashMap<RoaAggregateKey, Vec<RoaPayloadJsonMapKey>> {
let mut map: HashMap<RoaAggregateKey, Vec<RoaPayloadJsonMapKey>> =
HashMap::new();
for auth in self.map.keys() {
let key = RoaAggregateKey::new(auth.asn(), None);
if let Some(authorizations) = map.get_mut(&key) {
authorizations.push(*auth);
authorizations.sort();
} else {
map.insert(key, vec![*auth]);
}
}
map
}
pub fn add(&mut self, auth: RoaPayloadJsonMapKey) {
self.map.insert(auth, RouteInfo::default());
}
pub fn update_comment(
&mut self, auth: &RoaPayloadJsonMapKey, comment: Option<String>
) {
if let Some(info) = self.map.get_mut(auth) {
info.comment = comment
}
}
pub fn remove(&mut self, auth: &RoaPayloadJsonMapKey) -> bool {
self.map.remove(auth).is_some()
}
pub fn process_updates(
&self,
handle: &CaHandle,
all_resources: &ResourceSet,
updates: &RoaConfigurationUpdates,
) -> KrillResult<(Self, Vec<CertAuthEvent>)> {
let mut delta_errors = RoaDeltaError::default();
let mut res = vec![];
let mut desired_routes = self.clone();
for roa_payload in &updates.removed {
let auth = RoaPayloadJsonMapKey::from(*roa_payload);
if desired_routes.remove(&auth) {
res.push(CertAuthEvent::RouteAuthorizationRemoved { auth });
}
else {
delta_errors.add_unknown(*roa_payload)
}
}
for roa_configuration in &updates.added {
let roa_payload = roa_configuration.payload;
let comment = roa_configuration.comment.as_ref();
let auth = RoaPayloadJsonMapKey::from(roa_payload);
if !roa_payload.max_length_valid() {
delta_errors.add_invalid_length(roa_configuration.clone());
}
else if !all_resources.contains_roa_address(
&roa_payload.as_roa_ip_address()
) {
delta_errors.add_notheld(roa_configuration.clone());
}
else if let Some(info) = desired_routes.get(&auth) {
if info.comment.as_ref() != comment {
res.push(CertAuthEvent::RouteAuthorizationComment {
auth,
comment: comment.cloned(),
});
}
else {
delta_errors.add_duplicate(roa_configuration.clone());
}
}
else {
res.push(CertAuthEvent::RouteAuthorizationAdded { auth });
desired_routes.add(auth);
if comment.is_some() {
desired_routes.update_comment(
&auth, comment.cloned()
);
res.push(CertAuthEvent::RouteAuthorizationComment {
auth,
comment: comment.cloned(),
});
}
}
}
if !delta_errors.is_empty() {
Err(Error::RoaDeltaError(handle.clone(), delta_errors))
}
else {
Ok((desired_routes, res))
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RouteInfo {
pub since: Time,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<u32>,
}
impl Default for RouteInfo {
fn default() -> Self {
RouteInfo {
since: Time::now(),
comment: None,
group: None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RoaAggregateKey {
asn: AsNumber,
group: Option<u32>,
}
impl RoaAggregateKey {
pub fn new(asn: AsNumber, group: Option<u32>) -> Self {
RoaAggregateKey { asn, group }
}
pub fn asn(&self) -> AsNumber {
self.asn
}
pub fn group(&self) -> Option<u32> {
self.group
}
pub fn object_name(&self) -> ObjectName {
ObjectName::new(
match self.group {
None => format!("AS{}.roa", self.asn),
Some(number) => {
format!("AS{}-{}.roa", self.asn, number)
}
}
)
}
}
impl FromStr for RoaAggregateKey {
type Err = RoaAggregateKeyFmtError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split('-');
let asn_part = parts.next().ok_or_else(|| {
RoaAggregateKeyFmtError(s.into())
})?;
if !asn_part.starts_with("AS") || asn_part.len() < 3 {
return Err(RoaAggregateKeyFmtError(s.into()));
}
let asn = AsNumber::from_str(&asn_part[2..])
.map_err(|_| RoaAggregateKeyFmtError(s.into()))?;
let group = if let Some(group) = parts.next() {
let group = u32::from_str(group).map_err(|_| {
RoaAggregateKeyFmtError(s.into())
})?;
Some(group)
}
else {
None
};
if parts.next().is_some() {
return Err(RoaAggregateKeyFmtError(s.into()))
}
Ok(Self { asn, group })
}
}
impl PartialOrd for RoaAggregateKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RoaAggregateKey {
fn cmp(&self, other: &Self) -> Ordering {
match self.asn.cmp(&other.asn) {
Ordering::Equal => self.group.cmp(&other.group),
Ordering::Greater => Ordering::Greater,
Ordering::Less => Ordering::Less,
}
}
}
impl fmt::Display for RoaAggregateKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.group {
None => write!(f, "AS{}", self.asn),
Some(nr) => write!(f, "AS{}-{}", self.asn, nr),
}
}
}
impl<'de> Deserialize<'de> for RoaAggregateKey {
fn deserialize<D: Deserializer<'de>>(
d: D
) -> Result<RoaAggregateKey, D::Error> {
RoaAggregateKey::from_str(
String::deserialize(d)?.as_str()
).map_err(de::Error::custom)
}
}
impl Serialize for RoaAggregateKey {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.to_string().serialize(s)
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct Roas {
#[serde(
skip_serializing_if = "HashMap::is_empty",
default = "HashMap::new"
)]
simple: HashMap<RoaPayloadJsonMapKey, RoaInfo>,
#[serde(
skip_serializing_if = "HashMap::is_empty",
default = "HashMap::new"
)]
aggregate: HashMap<RoaAggregateKey, RoaInfo>,
}
impl Roas {
pub fn is_empty(&self) -> bool {
self.simple.is_empty() && self.aggregate.is_empty()
}
pub fn apply_updates(&mut self, updates: RoaUpdates) {
for (auth, info) in updates.updated.into_iter() {
self.simple.insert(auth, info);
}
for auth in updates.removed {
self.simple.remove(&auth);
}
for (key, aggregate) in updates.aggregate_updated.into_iter() {
self.aggregate.insert(key, aggregate);
}
for key in updates.aggregate_removed {
self.aggregate.remove(&key);
}
}
pub fn matching_roa_infos(
&self,
config: &RoaConfiguration,
) -> Vec<RoaInfo> {
let payload = RoaPayloadJsonMapKey::from(
config.payload.into_explicit_max_length(),
);
let mut roa_infos: Vec<RoaInfo> = self
.simple
.values()
.filter(|info| info.authorizations.contains(&payload))
.cloned()
.collect();
roa_infos.append(
&mut self
.aggregate
.values()
.filter(|info| info.authorizations.contains(&payload))
.cloned()
.collect(),
);
roa_infos
}
pub fn create_updates(
&self,
all_routes: &Routes,
certified_key: &CertifiedKey,
config: &Config,
signer: &KrillSigner,
) -> KrillResult<RoaUpdates> {
let relevant_routes =
all_routes.filter(&certified_key.incoming_cert().resources);
match self.mode(
relevant_routes.len(),
config.roa_deaggregate_threshold,
config.roa_aggregate_threshold,
) {
RoaMode::Simple => {
self.update_simple(
&relevant_routes,
certified_key,
&config.issuance_timing,
signer,
)
}
RoaMode::StopAggregating => {
self.update_stop_aggregating(
&relevant_routes,
certified_key,
&config.issuance_timing,
signer,
)
}
RoaMode::StartAggregating => {
self.update_start_aggregating(
&relevant_routes,
certified_key,
&config.issuance_timing,
signer,
)
}
RoaMode::Aggregate => {
self.update_aggregate(
&relevant_routes,
certified_key,
&config.issuance_timing,
signer,
)
}
}
}
fn is_currently_aggregating(&self) -> bool {
self.aggregate.keys().any(|k| k.group().is_none())
}
fn mode(
&self,
total: usize,
de_aggregation_threshold: usize,
aggregation_threshold: usize,
) -> RoaMode {
let mode = {
if total == 0 {
if self.is_currently_aggregating() {
RoaMode::Aggregate
}
else {
RoaMode::Simple
}
}
else if self.is_currently_aggregating() {
if total < de_aggregation_threshold {
RoaMode::StopAggregating
}
else {
RoaMode::Aggregate
}
}
else if total > aggregation_threshold {
RoaMode::StartAggregating
}
else {
RoaMode::Simple
}
};
debug!("Selecting ROA publication mode: {mode:?}");
mode
}
fn update_simple(
&self,
relevant_routes: &Routes,
certified_key: &CertifiedKey,
issuance_timing: &IssuanceTimingConfig,
signer: &KrillSigner,
) -> KrillResult<RoaUpdates> {
let mut roa_updates = RoaUpdates::default();
for auth in relevant_routes.roa_payload_keys() {
if !self.simple.contains_key(&auth) {
let name = ObjectName::from(auth);
let authorizations = vec![auth];
let roa = Self::make_roa(
&authorizations,
&name,
certified_key,
issuance_timing.new_roa_validity(),
signer,
)?;
let info = RoaInfo::new(authorizations, roa);
roa_updates.updated.insert(auth, info);
}
}
for auth in self.simple.keys() {
if !relevant_routes.has(auth) {
roa_updates.removed.push(*auth);
}
}
Ok(roa_updates)
}
fn update_stop_aggregating(
&self,
relevant_routes: &Routes,
certified_key: &CertifiedKey,
issuance_timing: &IssuanceTimingConfig,
signer: &KrillSigner,
) -> KrillResult<RoaUpdates> {
let mut roa_updates = self.update_simple(
relevant_routes,
certified_key,
issuance_timing,
signer,
)?;
for roa_key in self.aggregate.keys() {
roa_updates.aggregate_removed.push(*roa_key);
}
Ok(roa_updates)
}
fn update_start_aggregating(
&self,
relevant_routes: &Routes,
certified_key: &CertifiedKey,
issuance_timing: &IssuanceTimingConfig,
signer: &KrillSigner,
) -> KrillResult<RoaUpdates> {
let mut roa_updates = self.update_aggregate(
relevant_routes,
certified_key,
issuance_timing,
signer,
)?;
for roa_key in self.simple.keys() {
debug!("Will remove simple authorization for: {roa_key}");
roa_updates.removed.push(*roa_key);
}
Ok(roa_updates)
}
fn update_aggregate(
&self,
relevant_routes: &Routes,
certified_key: &CertifiedKey,
issuance_timing: &IssuanceTimingConfig,
signer: &KrillSigner,
) -> KrillResult<RoaUpdates> {
let mut roa_updates = RoaUpdates::default();
let desired_aggregates = relevant_routes.to_aggregates();
debug!("Will create '{}' aggregates", desired_aggregates.len());
for (key, authorizations) in desired_aggregates.iter() {
if let Some(existing) = self.aggregate.get(key) {
let mut existing_authorizations =
existing.authorizations.clone();
existing_authorizations.sort();
if authorizations != &existing_authorizations {
let aggregate = Self::make_aggregate_roa(
key,
authorizations.clone(),
certified_key,
issuance_timing,
signer,
)?;
roa_updates.aggregate_updated.insert(*key, aggregate);
}
}
else {
let aggregate = Self::make_aggregate_roa(
key,
authorizations.clone(),
certified_key,
issuance_timing,
signer,
)?;
roa_updates.aggregate_updated.insert(*key, aggregate);
}
}
for key in self.aggregate.keys() {
if !desired_aggregates.contains_key(key) {
roa_updates.aggregate_removed.push(*key);
}
}
Ok(roa_updates)
}
pub fn create_renewal(
&self,
force: bool,
certified_key: &CertifiedKey,
issuance_timing: &IssuanceTimingConfig,
signer: &KrillSigner,
) -> KrillResult<RoaUpdates> {
let mut updates = RoaUpdates::default();
let renew_threshold = issuance_timing.new_roa_issuance_threshold();
for (auth, roa_info) in self.simple.iter() {
let name = ObjectName::from(*auth);
if force || roa_info.expires() < renew_threshold {
let authorizations = vec![*auth];
let roa = Self::make_roa(
&authorizations,
&name,
certified_key,
issuance_timing.new_roa_validity(),
signer,
)?;
let new_roa_info = RoaInfo::new(authorizations, roa);
updates.updated.insert(*auth, new_roa_info);
}
}
for (roa_key, roa_info) in self.aggregate.iter() {
if force || roa_info.expires() < renew_threshold {
let authorizations = roa_info.authorizations.clone();
let name = roa_key.object_name();
let new_roa = Self::make_roa(
authorizations.as_slice(),
&name,
certified_key,
issuance_timing.new_roa_validity(),
signer,
)?;
let new_roa_info = RoaInfo::new(authorizations, new_roa);
updates.aggregate_updated.insert(*roa_key, new_roa_info);
}
}
Ok(updates)
}
fn make_aggregate_roa(
key: &RoaAggregateKey,
authorizations: Vec<RoaPayloadJsonMapKey>,
certified_key: &CertifiedKey,
issuance_timing: &IssuanceTimingConfig,
signer: &KrillSigner,
) -> KrillResult<RoaInfo> {
let name = key.object_name();
let roa = Self::make_roa(
&authorizations,
&name,
certified_key,
issuance_timing.new_roa_validity(),
signer,
)?;
Ok(RoaInfo::new(authorizations, roa))
}
fn make_roa(
authorizations: &[RoaPayloadJsonMapKey],
name: &ObjectName,
certified_key: &CertifiedKey,
validity: Validity,
signer: &KrillSigner,
) -> KrillResult<Roa> {
let crl_uri = certified_key.incoming_cert().crl_uri();
let roa_uri = certified_key.incoming_cert().uri_for_name(name);
let aia = &certified_key.incoming_cert().uri;
let asn = authorizations.first().ok_or_else(|| {
Error::custom("Attempt to create ROA without prefixes")
})?.as_ref().asn;
let mut roa_builder = RoaBuilder::new(asn.into());
for auth in authorizations {
let auth = RoaPayload::from(*auth);
if auth.asn != asn {
return Err(Error::custom(
"Attempt to create ROA for multiple ASNs",
));
}
let prefix = auth.prefix;
if auth.effective_max_length() > prefix.prefix().addr_len() {
roa_builder.push_addr(
prefix.ip_addr(),
prefix.addr_len(),
auth.max_length,
);
} else {
roa_builder.push_addr(
prefix.ip_addr(),
prefix.addr_len(),
None,
);
}
}
let mut object_builder = SignedObjectBuilder::new(
signer.random_serial()?,
validity,
crl_uri,
aia.clone(),
roa_uri,
);
object_builder.set_issuer(
Some(certified_key.incoming_cert().subject.clone())
);
object_builder.set_signing_time(Time::now());
Ok(signer.sign_roa(
roa_builder, object_builder, &certified_key.key_id()
)?)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum RoaMode {
Simple,
StopAggregating,
StartAggregating,
Aggregate,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct RoaUpdates {
#[serde(
skip_serializing_if = "HashMap::is_empty",
default = "HashMap::new"
)]
updated: HashMap<RoaPayloadJsonMapKey, RoaInfo>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
removed: Vec<RoaPayloadJsonMapKey>,
#[serde(
skip_serializing_if = "HashMap::is_empty",
default = "HashMap::new"
)]
aggregate_updated: HashMap<RoaAggregateKey, RoaInfo>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
aggregate_removed: Vec<RoaAggregateKey>,
}
impl RoaUpdates {
pub fn new(
updated: HashMap<RoaPayloadJsonMapKey, RoaInfo>,
removed: Vec<RoaPayloadJsonMapKey>,
aggregate_updated: HashMap<RoaAggregateKey, RoaInfo>,
aggregate_removed: Vec<RoaAggregateKey>,
) -> Self {
RoaUpdates {
updated,
removed,
aggregate_updated,
aggregate_removed,
}
}
pub fn is_empty(&self) -> bool {
self.updated.is_empty()
&& self.removed.is_empty()
&& self.aggregate_updated.is_empty()
&& self.aggregate_removed.is_empty()
}
pub fn added_roas(
&self
) -> impl Iterator<Item = (ObjectName, &RoaInfo)> + '_ {
self.updated.iter().map(|(auth, info)| {
(ObjectName::from(*auth), info)
}).chain(
self.aggregate_updated.iter().map(|(agg_key, info)| {
(agg_key.object_name(), info)
})
)
}
pub fn removed_roas(&self) -> impl Iterator<Item = ObjectName> + '_ {
self.removed.iter().map(|simple| ObjectName::from(*simple)).chain(
self.aggregate_removed.iter().map(|agg| agg.object_name())
)
}
pub fn fmt_event(&self, f: &mut fmt::Formatter) -> fmt::Result {
if !self.updated.is_empty()
|| !self.aggregate_updated.is_empty()
{
write!(f, " added: ")?;
for auth in self.updated.keys() {
write!(f, "{} ", ObjectName::from(*auth))?;
}
for agg_key in self.aggregate_updated.keys() {
write!(f, "{} ", agg_key.object_name())?;
}
}
if !self.removed.is_empty()
|| !self.aggregate_removed.is_empty()
{
write!(f, " removed: ")?;
for auth in &self.removed {
write!(f, "{} ", ObjectName::from(*auth))?;
}
for agg_key in &self.aggregate_removed {
write!(f, "{} ", agg_key.object_name())?;
}
}
Ok(())
}
}
impl fmt::Display for RoaUpdates {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if !self.updated.is_empty() {
write!(f, "Updated single VRP ROAs: ")?;
for roa in self.updated.keys() {
write!(f, "{} ", ObjectName::from(*roa))?;
}
}
if !self.removed.is_empty() {
write!(f, "Removed single VRP ROAs: ")?;
for roa in &self.removed {
write!(f, "{} ", ObjectName::from(*roa))?;
}
}
if !self.aggregate_updated.is_empty() {
write!(f, "Updated ASN aggregated ROAs: ")?;
for roa in self.aggregate_updated.keys() {
write!(f, "{} ", roa.object_name())?;
}
}
if !self.aggregate_removed.is_empty() {
write!(f, "Removed ASN aggregated ROAs: ")?;
for roa in &self.aggregate_removed {
write!(f, "{} ", roa.object_name())?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RoaAggregateKeyFmtError(String);
impl fmt::Display for RoaAggregateKeyFmtError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Invalid ROA Group format '{}'", self.0)
}
}
#[cfg(test)]
mod tests {
use crate::api::roa::{AsNumber, RoaPayload};
use super::*;
fn authorization(s: &str) -> RoaPayloadJsonMapKey {
RoaPayload::from_str(s).unwrap().into()
}
#[test]
fn serde_route_authorization() {
fn parse_encode_authorization(s: &str) {
let auth = authorization(s);
let json = serde_json::to_string(&auth).unwrap();
assert_eq!(format!("\"{s}\""), json);
let des: RoaPayloadJsonMapKey =
serde_json::from_str(&json).unwrap();
assert_eq!(des, auth);
}
parse_encode_authorization("192.168.0.0/16 => 64496");
parse_encode_authorization("192.168.0.0/16-24 => 64496");
parse_encode_authorization("2001:db8::/32 => 64496");
parse_encode_authorization("2001:db8::/32-48 => 64496");
}
#[test]
fn routes_as_aggregates() {
let mut routes = Routes::default();
let auth1_1 = authorization("192.168.0.0/16 => 64496");
let auth1_2 = authorization("192.168.0.0/16-24 => 64496");
let auth1_3 = authorization("2001:db8::/32 => 64496");
let auth2_1 = authorization("2001:db8::/32-48 => 64497");
routes.add(auth1_1);
routes.add(auth1_2);
routes.add(auth1_3);
routes.add(auth2_1);
let aggregates = routes.to_aggregates();
assert_eq!(2, aggregates.keys().len());
let agg_1 = aggregates
.get(&RoaAggregateKey::new(AsNumber::from_u32(64496), None))
.unwrap();
let mut agg_1_expected = vec![auth1_1, auth1_2, auth1_3];
agg_1_expected.sort();
assert_eq!(agg_1, &agg_1_expected);
let agg_2 = aggregates
.get(&RoaAggregateKey::new(AsNumber::from_u32(64497), None))
.unwrap();
assert_eq!(agg_2, &vec![auth2_1])
}
#[test]
fn roa_group_string() {
let roa_group_asn_only = RoaAggregateKey {
asn: AsNumber::from_u32(0),
group: None,
};
let roa_group_asn_only_expected_str = "AS0";
assert_eq!(
roa_group_asn_only.to_string().as_str(),
roa_group_asn_only_expected_str
);
let roa_group_asn_only_expected =
RoaAggregateKey::from_str(roa_group_asn_only_expected_str)
.unwrap();
assert_eq!(roa_group_asn_only, roa_group_asn_only_expected)
}
}