295 lines
9.0 KiB
Rust
295 lines
9.0 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use rpki::data_model::tal::Tal;
|
|
use rpki::fetch::rsync::{RsyncFetchError, RsyncFetcher};
|
|
use rpki::policy::{Policy, SyncPreference};
|
|
use rpki::storage::RocksStore;
|
|
use rpki::sync::rrdp::Fetcher;
|
|
use rpki::validation::from_tal::{
|
|
FromTalError, discover_root_ca_instance_from_tal,
|
|
discover_root_ca_instance_from_tal_and_ta_der, discover_root_ca_instance_from_tal_url,
|
|
run_root_from_tal_url_once,
|
|
};
|
|
use rpki::validation::manifest::PublicationPointSource;
|
|
use url::Url;
|
|
|
|
struct MapFetcher {
|
|
by_uri: HashMap<String, Vec<u8>>,
|
|
}
|
|
|
|
impl MapFetcher {
|
|
fn new(by_uri: HashMap<String, Vec<u8>>) -> Self {
|
|
Self { by_uri }
|
|
}
|
|
}
|
|
|
|
impl Fetcher for MapFetcher {
|
|
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
|
self.by_uri
|
|
.get(uri)
|
|
.cloned()
|
|
.ok_or_else(|| format!("missing mapping for {uri}"))
|
|
}
|
|
}
|
|
|
|
struct EmptyRsync;
|
|
|
|
impl RsyncFetcher for EmptyRsync {
|
|
fn fetch_objects(
|
|
&self,
|
|
_rsync_base_uri: &str,
|
|
) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
|
|
Ok(Vec::new())
|
|
}
|
|
}
|
|
|
|
fn openssl_available() -> bool {
|
|
std::process::Command::new("openssl")
|
|
.arg("version")
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn run(cmd: &mut std::process::Command) {
|
|
let out = cmd.output().expect("run command");
|
|
if !out.status.success() {
|
|
panic!(
|
|
"command failed: {:?}\nstdout={}\nstderr={}",
|
|
cmd,
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
}
|
|
|
|
fn apnic_tal_bytes() -> Vec<u8> {
|
|
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic TAL fixture")
|
|
}
|
|
|
|
fn apnic_ta_der() -> Vec<u8> {
|
|
std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic TA fixture")
|
|
}
|
|
|
|
#[test]
|
|
fn offline_discovery_from_apnic_tal_and_ta_der_fixture_works() {
|
|
let tal_bytes = apnic_tal_bytes();
|
|
let ta_der = apnic_ta_der();
|
|
|
|
let d = discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None)
|
|
.expect("discover root from fixtures");
|
|
|
|
assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://"));
|
|
assert!(d.ca_instance.rsync_base_uri.ends_with('/'));
|
|
assert!(d.ca_instance.manifest_rsync_uri.starts_with("rsync://"));
|
|
assert!(d.ca_instance.manifest_rsync_uri.ends_with(".mft"));
|
|
if let Some(n) = &d.ca_instance.rrdp_notification_uri {
|
|
assert!(n.starts_with("https://"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_from_tal_url_works_with_mock_fetcher() {
|
|
let tal_bytes = apnic_tal_bytes();
|
|
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
|
let ta_uri = tal.ta_uris[0].as_str().to_string();
|
|
|
|
let mut map = HashMap::new();
|
|
map.insert("https://example.test/apnic.tal".to_string(), tal_bytes);
|
|
map.insert(ta_uri, apnic_ta_der());
|
|
|
|
let fetcher = MapFetcher::new(map);
|
|
|
|
let d = discover_root_ca_instance_from_tal_url(&fetcher, "https://example.test/apnic.tal")
|
|
.expect("discover");
|
|
assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://"));
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_tries_multiple_ta_uris_until_one_succeeds() {
|
|
let tal_bytes = apnic_tal_bytes();
|
|
let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
|
let good_uri = tal.ta_uris[0].clone();
|
|
tal.ta_uris
|
|
.insert(0, Url::parse("https://example.invalid/bad.cer").unwrap());
|
|
|
|
let mut map = HashMap::new();
|
|
map.insert(good_uri.as_str().to_string(), apnic_ta_der());
|
|
let fetcher = MapFetcher::new(map);
|
|
|
|
let d = discover_root_ca_instance_from_tal(&fetcher, tal, None).expect("discover");
|
|
assert_eq!(d.trust_anchor.resolved_ta_uri.as_ref(), Some(&good_uri));
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_errors_when_no_ta_uris_present() {
|
|
let tal_bytes = apnic_tal_bytes();
|
|
let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
|
tal.ta_uris.clear();
|
|
|
|
let fetcher = MapFetcher::new(HashMap::new());
|
|
let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
|
|
assert!(matches!(err, FromTalError::NoTaUris));
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_errors_when_all_ta_fetches_fail() {
|
|
let tal_bytes = apnic_tal_bytes();
|
|
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
|
let fetcher = MapFetcher::new(HashMap::new());
|
|
let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
|
|
assert!(matches!(err, FromTalError::TaFetch(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_errors_when_ta_does_not_bind_to_tal_spki() {
|
|
let tal_bytes = apnic_tal_bytes();
|
|
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
|
|
|
// Use a different TA cert fixture to trigger SPKI mismatch.
|
|
let wrong_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta");
|
|
|
|
let mut map = HashMap::new();
|
|
map.insert(tal.ta_uris[0].as_str().to_string(), wrong_ta);
|
|
let fetcher = MapFetcher::new(map);
|
|
|
|
let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
|
|
assert!(matches!(err, FromTalError::TaFetch(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_from_tal_url_errors_when_tal_fetch_fails() {
|
|
let fetcher = MapFetcher::new(HashMap::new());
|
|
let err = discover_root_ca_instance_from_tal_url(&fetcher, "https://example.test/missing.tal")
|
|
.unwrap_err();
|
|
assert!(matches!(err, FromTalError::TalFetch(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn run_root_from_tal_url_once_returns_failed_fetch_no_cache_when_repo_is_empty() {
|
|
let tal_bytes = apnic_tal_bytes();
|
|
let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
|
|
let ta_uri = tal.ta_uris[0].as_str().to_string();
|
|
|
|
let mut map = HashMap::new();
|
|
map.insert("https://example.test/apnic.tal".to_string(), tal_bytes);
|
|
map.insert(ta_uri, apnic_ta_der());
|
|
let fetcher = MapFetcher::new(map);
|
|
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
|
|
|
let mut policy = Policy::default();
|
|
policy.sync_preference = SyncPreference::RsyncOnly;
|
|
|
|
let out = run_root_from_tal_url_once(
|
|
&store,
|
|
&policy,
|
|
"https://example.test/apnic.tal",
|
|
&fetcher,
|
|
&EmptyRsync,
|
|
time::OffsetDateTime::now_utc(),
|
|
)
|
|
.expect("run should return failed-fetch-no-cache output");
|
|
|
|
assert_eq!(
|
|
out.run.publication_point_source,
|
|
PublicationPointSource::FailedFetchNoCache
|
|
);
|
|
assert!(out.run.objects.vrps.is_empty());
|
|
assert!(out.run.objects.aspas.is_empty());
|
|
assert!(
|
|
out.run
|
|
.publication_point_warnings
|
|
.iter()
|
|
.any(|warning| warning.message.contains("no latest validated result"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn discover_root_records_ca_instance_discovery_failure_when_ta_lacks_sia() {
|
|
use base64::Engine;
|
|
|
|
assert!(openssl_available(), "openssl is required for this test");
|
|
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let dir = temp.path();
|
|
std::fs::write(
|
|
dir.join("openssl.cnf"),
|
|
r#"
|
|
[ req ]
|
|
prompt = no
|
|
distinguished_name = dn
|
|
x509_extensions = v3_ta
|
|
|
|
[ dn ]
|
|
CN = Test TA Without SIA
|
|
|
|
[ v3_ta ]
|
|
basicConstraints = critical,CA:true
|
|
keyUsage = critical, keyCertSign, cRLSign
|
|
subjectKeyIdentifier = hash
|
|
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
|
|
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
|
|
sbgp-autonomousSysNum = critical, AS:64496-64511
|
|
"#,
|
|
)
|
|
.expect("write openssl cnf");
|
|
|
|
run(std::process::Command::new("openssl")
|
|
.arg("genrsa")
|
|
.arg("-out")
|
|
.arg(dir.join("ta.key"))
|
|
.arg("2048"));
|
|
run(std::process::Command::new("openssl")
|
|
.arg("req")
|
|
.arg("-new")
|
|
.arg("-x509")
|
|
.arg("-sha256")
|
|
.arg("-days")
|
|
.arg("365")
|
|
.arg("-key")
|
|
.arg(dir.join("ta.key"))
|
|
.arg("-config")
|
|
.arg(dir.join("openssl.cnf"))
|
|
.arg("-extensions")
|
|
.arg("v3_ta")
|
|
.arg("-out")
|
|
.arg(dir.join("ta.pem")));
|
|
run(std::process::Command::new("openssl")
|
|
.arg("x509")
|
|
.arg("-in")
|
|
.arg(dir.join("ta.pem"))
|
|
.arg("-outform")
|
|
.arg("DER")
|
|
.arg("-out")
|
|
.arg(dir.join("ta.cer")));
|
|
run(std::process::Command::new("sh").arg("-c").arg(format!(
|
|
"openssl x509 -in {} -pubkey -noout | openssl pkey -pubin -outform DER > {}",
|
|
dir.join("ta.pem").display(),
|
|
dir.join("spki.der").display(),
|
|
)));
|
|
|
|
let ta_uri = "https://example.test/no-sia-ta.cer";
|
|
let tal_text = format!(
|
|
"{ta_uri}\n\n{}\n",
|
|
base64::engine::general_purpose::STANDARD
|
|
.encode(std::fs::read(dir.join("spki.der")).expect("read spki der"))
|
|
);
|
|
let tal = Tal::decode_bytes(tal_text.as_bytes()).expect("decode generated tal");
|
|
|
|
let mut map = HashMap::new();
|
|
map.insert(
|
|
ta_uri.to_string(),
|
|
std::fs::read(dir.join("ta.cer")).expect("read ta der"),
|
|
);
|
|
let fetcher = MapFetcher::new(map);
|
|
|
|
let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
|
|
assert!(matches!(err, FromTalError::TaFetch(_)), "{err}");
|
|
assert!(
|
|
err.to_string().contains("CA instance discovery failed"),
|
|
"{err}"
|
|
);
|
|
}
|