557 lines
17 KiB
Rust
557 lines
17 KiB
Rust
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<String>,
|
|
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<DiscoveredRootCaInstance, FromTalError> {
|
|
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_url_with_strict_name(
|
|
http_fetcher: &dyn Fetcher,
|
|
tal_url: &str,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
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_with_strict_name(
|
|
http_fetcher,
|
|
tal,
|
|
Some(tal_url.to_string()),
|
|
)
|
|
}
|
|
|
|
pub fn discover_root_ca_instance_from_tal_url_with_fetchers(
|
|
http_fetcher: &dyn Fetcher,
|
|
rsync_fetcher: &dyn RsyncFetcher,
|
|
tal_url: &str,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
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_with_fetchers_impl(
|
|
http_fetcher,
|
|
rsync_fetcher,
|
|
tal,
|
|
Some(tal_url.to_string()),
|
|
false,
|
|
TaUriOrder::HttpFirst,
|
|
)
|
|
}
|
|
|
|
pub fn discover_root_ca_instance_from_tal_url_with_fetchers_strict_name(
|
|
http_fetcher: &dyn Fetcher,
|
|
rsync_fetcher: &dyn RsyncFetcher,
|
|
tal_url: &str,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
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_with_fetchers_impl(
|
|
http_fetcher,
|
|
rsync_fetcher,
|
|
tal,
|
|
Some(tal_url.to_string()),
|
|
true,
|
|
TaUriOrder::HttpFirst,
|
|
)
|
|
}
|
|
|
|
pub fn discover_root_ca_instance_from_tal(
|
|
http_fetcher: &dyn Fetcher,
|
|
tal: Tal,
|
|
tal_url: Option<String>,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
discover_root_ca_instance_from_tal_impl(http_fetcher, tal, tal_url, false)
|
|
}
|
|
|
|
pub fn discover_root_ca_instance_from_tal_with_strict_name(
|
|
http_fetcher: &dyn Fetcher,
|
|
tal: Tal,
|
|
tal_url: Option<String>,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
discover_root_ca_instance_from_tal_impl(http_fetcher, tal, tal_url, true)
|
|
}
|
|
|
|
fn discover_root_ca_instance_from_tal_impl(
|
|
http_fetcher: &dyn Fetcher,
|
|
tal: Tal,
|
|
tal_url: Option<String>,
|
|
strict_name: bool,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
if tal.ta_uris.is_empty() {
|
|
return Err(FromTalError::NoTaUris);
|
|
}
|
|
|
|
let mut last_err: Option<String> = 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 bind_trust_anchor_der(tal.clone(), &ta_der, Some(ta_uri), strict_name) {
|
|
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<String>,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
discover_root_ca_instance_from_tal_with_fetchers_impl(
|
|
http_fetcher,
|
|
rsync_fetcher,
|
|
tal,
|
|
tal_url,
|
|
false,
|
|
TaUriOrder::RsyncFirst,
|
|
)
|
|
}
|
|
|
|
pub fn discover_root_ca_instance_from_tal_with_fetchers_strict_name(
|
|
http_fetcher: &dyn Fetcher,
|
|
rsync_fetcher: &dyn RsyncFetcher,
|
|
tal: Tal,
|
|
tal_url: Option<String>,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
discover_root_ca_instance_from_tal_with_fetchers_impl(
|
|
http_fetcher,
|
|
rsync_fetcher,
|
|
tal,
|
|
tal_url,
|
|
true,
|
|
TaUriOrder::RsyncFirst,
|
|
)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum TaUriOrder {
|
|
HttpFirst,
|
|
RsyncFirst,
|
|
}
|
|
|
|
fn discover_root_ca_instance_from_tal_with_fetchers_impl(
|
|
http_fetcher: &dyn Fetcher,
|
|
rsync_fetcher: &dyn RsyncFetcher,
|
|
tal: Tal,
|
|
tal_url: Option<String>,
|
|
strict_name: bool,
|
|
ta_uri_order: TaUriOrder,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
if tal.ta_uris.is_empty() {
|
|
return Err(FromTalError::NoTaUris);
|
|
}
|
|
|
|
let mut last_err: Option<String> = None;
|
|
let mut ta_uris = tal.ta_uris.clone();
|
|
ta_uris.sort_by_key(|uri| ta_uri_priority(uri, ta_uri_order));
|
|
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 bind_trust_anchor_der(tal.clone(), &ta_der, Some(ta_uri), strict_name) {
|
|
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 ta_uri_priority(uri: &Url, order: TaUriOrder) -> u8 {
|
|
match (order, uri.scheme()) {
|
|
(TaUriOrder::HttpFirst, "https" | "http") => 0,
|
|
(TaUriOrder::HttpFirst, "rsync") => 1,
|
|
(TaUriOrder::RsyncFirst, "rsync") => 0,
|
|
(TaUriOrder::RsyncFirst, "https" | "http") => 1,
|
|
_ => 2,
|
|
}
|
|
}
|
|
|
|
fn fetch_ta_der(
|
|
http_fetcher: &dyn Fetcher,
|
|
rsync_fetcher: &dyn RsyncFetcher,
|
|
ta_uri: &Url,
|
|
) -> Result<Vec<u8>, 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 bind_trust_anchor_der(
|
|
tal: Tal,
|
|
ta_der: &[u8],
|
|
resolved_uri: Option<&Url>,
|
|
strict_name: bool,
|
|
) -> Result<TrustAnchor, TrustAnchorError> {
|
|
if strict_name {
|
|
TrustAnchor::bind_der_with_strict_name(tal, ta_der, resolved_uri)
|
|
} else {
|
|
TrustAnchor::bind_der(tal, ta_der, resolved_uri)
|
|
}
|
|
}
|
|
|
|
fn fetch_ta_der_via_rsync(
|
|
rsync_fetcher: &dyn RsyncFetcher,
|
|
ta_rsync_uri: &str,
|
|
) -> Result<Vec<u8>, String> {
|
|
rsync_fetcher
|
|
.fetch_object(ta_rsync_uri)
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
pub fn discover_root_ca_instance_from_tal_and_ta_der(
|
|
tal_bytes: &[u8],
|
|
ta_der: &[u8],
|
|
resolved_ta_uri: Option<&Url>,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
discover_root_ca_instance_from_tal_and_ta_der_impl(tal_bytes, ta_der, resolved_ta_uri, false)
|
|
}
|
|
|
|
pub fn discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
|
|
tal_bytes: &[u8],
|
|
ta_der: &[u8],
|
|
resolved_ta_uri: Option<&Url>,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
discover_root_ca_instance_from_tal_and_ta_der_impl(tal_bytes, ta_der, resolved_ta_uri, true)
|
|
}
|
|
|
|
fn discover_root_ca_instance_from_tal_and_ta_der_impl(
|
|
tal_bytes: &[u8],
|
|
ta_der: &[u8],
|
|
resolved_ta_uri: Option<&Url>,
|
|
strict_name: bool,
|
|
) -> Result<DiscoveredRootCaInstance, FromTalError> {
|
|
let tal = Tal::decode_bytes(tal_bytes)?;
|
|
let trust_anchor = bind_trust_anchor_der(tal, ta_der, resolved_ta_uri, strict_name)?;
|
|
let ca_instance = ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)?;
|
|
Ok(DiscoveredRootCaInstance {
|
|
tal_url: None,
|
|
trust_anchor,
|
|
ca_instance,
|
|
})
|
|
}
|
|
|
|
pub fn canonical_tal_rsync_uri_from_bytes(tal_bytes: &[u8]) -> Result<Url, FromTalError> {
|
|
let tal = Tal::decode_bytes(tal_bytes)?;
|
|
tal.ta_uris
|
|
.iter()
|
|
.find(|uri| uri.scheme() == "rsync")
|
|
.cloned()
|
|
.ok_or_else(|| {
|
|
FromTalError::TaFetch("TAL contains no rsync TA URI for offline TA binding".to_string())
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
|
use std::collections::HashMap;
|
|
|
|
struct MapHttpFetcher {
|
|
map: HashMap<String, Vec<u8>>,
|
|
}
|
|
|
|
impl Fetcher for MapHttpFetcher {
|
|
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
|
self.map
|
|
.get(uri)
|
|
.cloned()
|
|
.ok_or_else(|| format!("no fixture mapped for {uri}"))
|
|
}
|
|
}
|
|
|
|
struct FailingHttpFetcher;
|
|
|
|
impl Fetcher for FailingHttpFetcher {
|
|
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
|
Err(format!("blocked test HTTP fetch: {uri}"))
|
|
}
|
|
}
|
|
|
|
#[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 rsync = LocalDirRsyncFetcher::new(
|
|
td.path()
|
|
.join(rsync_uri.host_str().unwrap())
|
|
.join("repository"),
|
|
);
|
|
let discovery = discover_root_ca_instance_from_tal_with_fetchers(
|
|
&FailingHttpFetcher,
|
|
&rsync,
|
|
tal,
|
|
None,
|
|
)
|
|
.expect("discover via rsync TA fallback");
|
|
assert!(
|
|
discovery
|
|
.trust_anchor
|
|
.resolved_ta_uri
|
|
.unwrap()
|
|
.as_str()
|
|
.starts_with("rsync://")
|
|
);
|
|
}
|
|
|
|
struct PanicRsyncFetcher;
|
|
|
|
impl RsyncFetcher for PanicRsyncFetcher {
|
|
fn fetch_objects(
|
|
&self,
|
|
rsync_base_uri: &str,
|
|
) -> crate::fetch::rsync::RsyncFetchResult<Vec<(String, Vec<u8>)>> {
|
|
panic!("rsync should not be used when HTTPS TA URI is available: {rsync_base_uri}")
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_ca_instance_from_tal_url_with_fetchers_prefers_https_ta_uri() {
|
|
let tal_url = "https://example.test/apnic.tal";
|
|
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 https_uri = "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer";
|
|
let mut map = HashMap::new();
|
|
map.insert(tal_url.to_string(), tal_bytes);
|
|
map.insert(https_uri.to_string(), ta_der);
|
|
let discovery = discover_root_ca_instance_from_tal_url_with_fetchers(
|
|
&MapHttpFetcher { map },
|
|
&PanicRsyncFetcher,
|
|
tal_url,
|
|
)
|
|
.expect("discover via HTTPS TA URI");
|
|
assert_eq!(
|
|
discovery.trust_anchor.resolved_ta_uri.unwrap().as_str(),
|
|
https_uri
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_ca_instance_from_tal_url_with_fetchers_supports_rsync_only_tal() {
|
|
let tal_url = "https://example.test/apnic-rsync-only.tal";
|
|
let tal_bytes = std::fs::read_to_string(
|
|
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures/tal/apnic-rfc7730-https.tal"),
|
|
)
|
|
.unwrap();
|
|
let rsync_uri = "rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer";
|
|
let key_material = tal_bytes
|
|
.split_once("\n\n")
|
|
.map(|(_, key)| key)
|
|
.expect("fixture contains TAL key material");
|
|
let rsync_only_tal = format!("{rsync_uri}\n\n{key_material}");
|
|
let ta_der = std::fs::read(
|
|
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures/ta/apnic-ta.cer"),
|
|
)
|
|
.unwrap();
|
|
let td = tempfile::tempdir().unwrap();
|
|
let mirror_root = td.path().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 mut map = HashMap::new();
|
|
map.insert(tal_url.to_string(), rsync_only_tal.into_bytes());
|
|
let discovery = discover_root_ca_instance_from_tal_url_with_fetchers(
|
|
&MapHttpFetcher { map },
|
|
&LocalDirRsyncFetcher::new(mirror_root),
|
|
tal_url,
|
|
)
|
|
.expect("discover via rsync-only TAL");
|
|
assert_eq!(
|
|
discovery.trust_anchor.resolved_ta_uri.unwrap().as_str(),
|
|
rsync_uri
|
|
);
|
|
}
|
|
}
|
|
|
|
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<RunFromTalOutput, FromTalError> {
|
|
let discovery =
|
|
discover_root_ca_instance_from_tal_url_with_fetchers(http_fetcher, rsync_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 })
|
|
}
|