Files
rpki/src/validation/from_tal.rs

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 })
}