use url::Url; use crate::data_model::ta::{TrustAnchor, TrustAnchorError}; use crate::data_model::tal::{Tal, TalDecodeError}; use crate::fetch::rsync::RsyncFetcher; use crate::sync::rrdp::Fetcher; use crate::validation::ca_instance::{ CaInstanceUris, CaInstanceUrisError, ca_instance_uris_from_ca_certificate, }; use crate::validation::run::{RunError, RunOutput, run_publication_point_once}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct DiscoveredRootCaInstance { pub tal_url: Option, pub trust_anchor: TrustAnchor, pub ca_instance: CaInstanceUris, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RunFromTalOutput { pub discovery: DiscoveredRootCaInstance, pub run: RunOutput, } #[derive(Debug, thiserror::Error)] pub enum FromTalError { #[error("TAL fetch failed: {0} (RFC 8630 §2.2)")] TalFetch(String), #[error("TAL decode failed: {0} (RFC 8630 §2.2)")] TalDecode(#[from] TalDecodeError), #[error("failed to fetch TA certificate from TAL: {0} (RFC 8630 §2.3)")] TaFetch(String), #[error("failed to bind TAL and TA certificate: {0} (RFC 8630 §2.3)")] Bind(#[from] TrustAnchorError), #[error("failed to discover CA instance URIs from TA certificate: {0}")] CaInstanceUris(#[from] CaInstanceUrisError), #[error("run failed: {0}")] Run(#[from] RunError), #[error("TAL contains no TA URIs (RFC 8630 §2.2)")] NoTaUris, } pub fn discover_root_ca_instance_from_tal_url( http_fetcher: &dyn Fetcher, tal_url: &str, ) -> Result { let tal_bytes = http_fetcher .fetch(tal_url) .map_err(FromTalError::TalFetch)?; let tal = Tal::decode_bytes(&tal_bytes)?; discover_root_ca_instance_from_tal(http_fetcher, tal, Some(tal_url.to_string())) } pub fn discover_root_ca_instance_from_tal( http_fetcher: &dyn Fetcher, tal: Tal, tal_url: Option, ) -> Result { if tal.ta_uris.is_empty() { return Err(FromTalError::NoTaUris); } let mut last_err: Option = None; for ta_uri in tal.ta_uris.iter() { let ta_der = match http_fetcher.fetch(ta_uri.as_str()) { Ok(b) => b, Err(e) => { last_err = Some(format!("fetch {ta_uri} failed: {e}")); continue; } }; let trust_anchor = match TrustAnchor::bind_der(tal.clone(), &ta_der, Some(ta_uri)) { Ok(ta) => ta, Err(e) => { last_err = Some(format!("bind {ta_uri} failed: {e}")); continue; } }; let ca_instance = match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) { Ok(v) => v, Err(e) => { last_err = Some(format!("CA instance discovery failed: {e}")); continue; } }; return Ok(DiscoveredRootCaInstance { tal_url, trust_anchor, ca_instance, }); } Err(FromTalError::TaFetch(last_err.unwrap_or_else(|| { "unknown TA candidate error".to_string() }))) } pub fn discover_root_ca_instance_from_tal_with_fetchers( http_fetcher: &dyn Fetcher, rsync_fetcher: &dyn RsyncFetcher, tal: Tal, tal_url: Option, ) -> Result { if tal.ta_uris.is_empty() { return Err(FromTalError::NoTaUris); } let mut last_err: Option = None; let mut ta_uris = tal.ta_uris.clone(); ta_uris.sort_by_key(|uri| if uri.scheme() == "rsync" { 0 } else { 1 }); for ta_uri in ta_uris.iter() { let ta_der = match fetch_ta_der(http_fetcher, rsync_fetcher, ta_uri) { Ok(b) => b, Err(e) => { last_err = Some(format!("fetch {ta_uri} failed: {e}")); continue; } }; let trust_anchor = match TrustAnchor::bind_der(tal.clone(), &ta_der, Some(ta_uri)) { Ok(ta) => ta, Err(e) => { last_err = Some(format!("bind {ta_uri} failed: {e}")); continue; } }; let ca_instance = match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) { Ok(v) => v, Err(e) => { last_err = Some(format!("CA instance discovery failed: {e}")); continue; } }; return Ok(DiscoveredRootCaInstance { tal_url, trust_anchor, ca_instance, }); } Err(FromTalError::TaFetch(last_err.unwrap_or_else(|| { "unknown TA candidate error".to_string() }))) } fn fetch_ta_der( http_fetcher: &dyn Fetcher, rsync_fetcher: &dyn RsyncFetcher, ta_uri: &Url, ) -> Result, String> { match ta_uri.scheme() { "https" | "http" => http_fetcher.fetch(ta_uri.as_str()), "rsync" => fetch_ta_der_via_rsync(rsync_fetcher, ta_uri.as_str()), scheme => Err(format!("unsupported TA URI scheme: {scheme}")), } } fn fetch_ta_der_via_rsync( rsync_fetcher: &dyn RsyncFetcher, ta_rsync_uri: &str, ) -> Result, String> { let base = rsync_parent_uri(ta_rsync_uri)?; let objects = rsync_fetcher .fetch_objects(&base) .map_err(|e| e.to_string())?; objects .into_iter() .find(|(uri, _)| uri == ta_rsync_uri) .map(|(_, bytes)| bytes) .ok_or_else(|| format!("TA rsync object not found in fetched subtree: {ta_rsync_uri}")) } fn rsync_parent_uri(ta_rsync_uri: &str) -> Result { let url = Url::parse(ta_rsync_uri).map_err(|e| e.to_string())?; if url.scheme() != "rsync" { return Err(format!("not an rsync URI: {ta_rsync_uri}")); } let host = url .host_str() .ok_or_else(|| format!("missing host in rsync URI: {ta_rsync_uri}"))?; let segments = url .path_segments() .ok_or_else(|| format!("missing path in rsync URI: {ta_rsync_uri}"))? .collect::>(); if segments.is_empty() || segments.last().copied().unwrap_or_default().is_empty() { return Err(format!("rsync URI must reference a file object: {ta_rsync_uri}")); } let parent_segments = &segments[..segments.len() - 1]; let mut parent = format!("rsync://{host}/"); if !parent_segments.is_empty() { parent.push_str(&parent_segments.join("/")); parent.push('/'); } Ok(parent) } pub fn discover_root_ca_instance_from_tal_and_ta_der( tal_bytes: &[u8], ta_der: &[u8], resolved_ta_uri: Option<&Url>, ) -> Result { let tal = Tal::decode_bytes(tal_bytes)?; let trust_anchor = TrustAnchor::bind_der(tal, ta_der, resolved_ta_uri)?; let ca_instance = ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)?; Ok(DiscoveredRootCaInstance { tal_url: None, trust_anchor, ca_instance, }) } #[cfg(test)] mod tests { use super::*; use crate::fetch::rsync::LocalDirRsyncFetcher; #[test] fn discover_root_ca_instance_from_tal_with_fetchers_supports_rsync_ta_uri() { let tal_bytes = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/tal/apnic-rfc7730-https.tal"), ) .unwrap(); let ta_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/ta/apnic-ta.cer"), ) .unwrap(); let tal = Tal::decode_bytes(&tal_bytes).unwrap(); let rsync_uri = tal .ta_uris .iter() .find(|uri| uri.scheme() == "rsync") .unwrap() .clone(); let td = tempfile::tempdir().unwrap(); let mirror_root = td.path().join(rsync_uri.host_str().unwrap()).join("repository"); std::fs::create_dir_all(&mirror_root).unwrap(); std::fs::write( mirror_root.join("apnic-rpki-root-iana-origin.cer"), ta_der, ) .unwrap(); let http = crate::fetch::http::BlockingHttpFetcher::new( crate::fetch::http::HttpFetcherConfig::default(), ) .unwrap(); let rsync = LocalDirRsyncFetcher::new( td.path().join(rsync_uri.host_str().unwrap()).join("repository"), ); let discovery = discover_root_ca_instance_from_tal_with_fetchers(&http, &rsync, tal, None) .expect("discover via rsync TA"); assert!(discovery .trust_anchor .resolved_ta_uri .unwrap() .as_str() .starts_with("rsync://")); } } pub fn run_root_from_tal_url_once( store: &crate::storage::RocksStore, policy: &crate::policy::Policy, tal_url: &str, http_fetcher: &dyn Fetcher, rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher, validation_time: time::OffsetDateTime, ) -> Result { let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?; let run = run_publication_point_once( store, policy, discovery.ca_instance.rrdp_notification_uri.as_deref(), &discovery.ca_instance.rsync_base_uri, &discovery.ca_instance.manifest_rsync_uri, &discovery.ca_instance.publication_point_rsync_uri, http_fetcher, rsync_fetcher, &discovery.trust_anchor.ta_certificate.raw_der, None, discovery .trust_anchor .ta_certificate .rc_ca .tbs .extensions .ip_resources .as_ref(), discovery .trust_anchor .ta_certificate .rc_ca .tbs .extensions .as_resources .as_ref(), validation_time, )?; Ok(RunFromTalOutput { discovery, run }) }