rpki/tests/test_ca_path_m15.rs
2026-02-10 12:09:59 +08:00

470 lines
16 KiB
Rust

use std::process::Command;
use rpki::data_model::rc::ResourceCertificate;
use rpki::validation::ca_path::{CaPathError, validate_subordinate_ca_cert};
fn openssl_available() -> bool {
Command::new("openssl")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
struct Generated {
issuer_ca_der: Vec<u8>,
child_ca_der: Vec<u8>,
issuer_crl_der: Vec<u8>,
}
fn write(path: &std::path::Path, s: &str) {
std::fs::write(path, s.as_bytes()).expect("write file");
}
fn run(cmd: &mut 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 generate_chain_and_crl(child_ext: &str, revoke_child: bool) -> Generated {
assert!(openssl_available(), "openssl is required for this test");
let td = tempfile::tempdir().expect("tempdir");
let dir = td.path();
// Minimal CA database layout required by `openssl ca`.
std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts");
std::fs::write(dir.join("index.txt"), b"").expect("index");
std::fs::write(dir.join("serial"), b"1000\n").expect("serial");
std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber");
let cnf = format!(
r#"
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = {dir}
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/issuer.pem
private_key = $dir/issuer.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 1
policy = policy_any
x509_extensions = v3_issuer_ca
crl_extensions = crl_ext
unique_subject = no
copy_extensions = none
[ policy_any ]
commonName = supplied
[ req ]
prompt = no
distinguished_name = dn
[ dn ]
CN = Test Issuer CA
[ v3_issuer_ca ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
sbgp-autonomousSysNum = critical, AS:64496-64511
[ v3_child_ca ]
basicConstraints = critical,CA:true
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/child/, rpkiManifest;URI:rsync://example.test/repo/child/child.mft, rpkiNotify;URI:https://example.test/notification.xml
{child_ext}
[ crl_ext ]
authorityKeyIdentifier = keyid:always
"#,
dir = dir.display(),
child_ext = child_ext
);
write(&dir.join("openssl.cnf"), &cnf);
// Issuer CA key + self-signed CA cert (DER later).
run(Command::new("openssl")
.arg("genrsa")
.arg("-out")
.arg(dir.join("issuer.key"))
.arg("2048"));
run(Command::new("openssl")
.arg("req")
.arg("-new")
.arg("-x509")
.arg("-sha256")
.arg("-days")
.arg("365")
.arg("-key")
.arg(dir.join("issuer.key"))
.arg("-out")
.arg(dir.join("issuer.pem"))
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-extensions")
.arg("v3_issuer_ca"));
// Child CA key + CSR.
run(Command::new("openssl")
.arg("genrsa")
.arg("-out")
.arg(dir.join("child.key"))
.arg("2048"));
run(Command::new("openssl")
.arg("req")
.arg("-new")
.arg("-key")
.arg(dir.join("child.key"))
.arg("-subj")
.arg("/CN=Child CA")
.arg("-out")
.arg(dir.join("child.csr"))
.arg("-config")
.arg(dir.join("openssl.cnf")));
// Issue child CA cert using openssl ca (so it appears in the CA database for CRL).
run(Command::new("openssl")
.arg("ca")
.arg("-batch")
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-extensions")
.arg("v3_child_ca")
.arg("-in")
.arg(dir.join("child.csr"))
.arg("-out")
.arg(dir.join("child.pem"))
.arg("-notext"));
if revoke_child {
run(Command::new("openssl")
.arg("ca")
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-revoke")
.arg(dir.join("child.pem")));
}
// Generate CRL.
run(Command::new("openssl")
.arg("ca")
.arg("-gencrl")
.arg("-config")
.arg(dir.join("openssl.cnf"))
.arg("-out")
.arg(dir.join("issuer.crl.pem")));
// Convert to DER.
run(Command::new("openssl")
.arg("x509")
.arg("-in")
.arg(dir.join("issuer.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("issuer.cer")));
run(Command::new("openssl")
.arg("x509")
.arg("-in")
.arg(dir.join("child.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("child.cer")));
run(Command::new("openssl")
.arg("crl")
.arg("-in")
.arg(dir.join("issuer.crl.pem"))
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(dir.join("issuer.crl")));
Generated {
issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
child_ca_der: std::fs::read(dir.join("child.cer")).expect("read child der"),
issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
}
}
#[test]
fn validate_subordinate_ca_succeeds_for_valid_child_and_subset_resources() {
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\nsbgp-autonomousSysNum = critical, AS:64496\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let validated = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.expect("validate subordinate");
assert!(validated.effective_ip_resources.is_some());
assert!(validated.effective_as_resources.is_some());
}
#[test]
fn validate_subordinate_ca_rejects_wrong_key_usage_bits() {
let generated = generate_chain_and_crl(
"keyUsage = critical, digitalSignature\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::KeyUsageInvalidBits));
}
#[test]
fn validate_subordinate_ca_rejects_out_of_scope_resources() {
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:11.0.0.0/8\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::ResourcesNotSubset));
}
#[test]
fn validate_subordinate_ca_rejects_revoked_child() {
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
true,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::ChildRevoked));
}
#[test]
fn validate_subordinate_ca_rejects_missing_key_usage_extension() {
let generated = generate_chain_and_crl(
"sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\nsbgp-autonomousSysNum = critical, AS:64496\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::KeyUsageMissing));
}
#[test]
fn validate_subordinate_ca_rejects_non_critical_key_usage() {
let generated = generate_chain_and_crl(
"keyUsage = keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::KeyUsageNotCritical));
}
#[test]
fn validate_subordinate_ca_rejects_when_child_has_no_resources() {
let generated = generate_chain_and_crl("keyUsage = critical, keyCertSign, cRLSign\n", false);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::ResourcesMissing));
}
#[test]
fn validate_subordinate_ca_rejects_when_cert_not_valid_at_validation_time() {
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let validation_time = time::OffsetDateTime::now_utc() + time::Duration::days(400);
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
validation_time,
)
.unwrap_err();
assert!(matches!(err, CaPathError::CertificateNotValidAtTime));
}
#[test]
fn validate_subordinate_ca_rejects_when_crl_not_valid_at_validation_time() {
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let validation_time = time::OffsetDateTime::now_utc() + time::Duration::days(2);
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
validation_time,
)
.unwrap_err();
assert!(matches!(err, CaPathError::CrlNotValidAtTime));
}
#[test]
fn validate_subordinate_ca_rejects_tampered_child_signature() {
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let mut tampered = generated.child_ca_der.clone();
if let Some(last) = tampered.last_mut() {
*last ^= 0x01;
}
let err = validate_subordinate_ca_cert(
&tampered,
&generated.issuer_ca_der,
&generated.issuer_crl_der,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::ChildSignatureInvalid(_)));
}
#[test]
fn validate_subordinate_ca_rejects_tampered_crl_signature() {
let generated = generate_chain_and_crl(
"keyUsage = critical, keyCertSign, cRLSign\nsbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16\n",
false,
);
let issuer = ResourceCertificate::decode_der(&generated.issuer_ca_der).expect("decode issuer");
let now = time::OffsetDateTime::now_utc();
let mut tampered = generated.issuer_crl_der.clone();
if let Some(last) = tampered.last_mut() {
*last ^= 0x01;
}
let err = validate_subordinate_ca_cert(
&generated.child_ca_der,
&generated.issuer_ca_der,
&tampered,
Some("rsync://example.test/repo/issuer/issuer.cer"),
"rsync://example.test/repo/issuer/issuer.crl",
issuer.tbs.extensions.ip_resources.as_ref(),
issuer.tbs.extensions.as_resources.as_ref(),
now,
)
.unwrap_err();
assert!(matches!(err, CaPathError::CrlVerify(_)));
}