use std::process::Command; use rpki::validation::cert_path::{CertPathError, validate_ee_cert_path}; fn openssl_available() -> bool { Command::new("openssl") .arg("version") .output() .map(|o| o.status.success()) .unwrap_or(false) } 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) ); } } struct Generated { issuer_ca_der: Vec, ee_der: Vec, issuer_crl_der: Vec, } fn generate_issuer_ca_ee_and_crl(ee_ext: &str) -> Generated { assert!(openssl_available(), "openssl is required for this test"); let td = tempfile::tempdir().expect("tempdir"); let dir = td.path(); 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 [ v3_ee ] basicConstraints = critical,CA:false subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer {ee_ext} [ crl_ext ] authorityKeyIdentifier = keyid:always "#, dir = dir.display(), ee_ext = ee_ext ); std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf"); 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("-config") .arg(dir.join("openssl.cnf")) .arg("-extensions") .arg("v3_issuer_ca") .arg("-out") .arg(dir.join("issuer.pem"))); run(Command::new("openssl") .arg("genrsa") .arg("-out") .arg(dir.join("ee.key")) .arg("2048")); run(Command::new("openssl") .arg("req") .arg("-new") .arg("-key") .arg(dir.join("ee.key")) .arg("-subj") .arg("/CN=Test EE") .arg("-out") .arg(dir.join("ee.csr"))); run(Command::new("openssl") .arg("ca") .arg("-batch") .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-in") .arg(dir.join("ee.csr")) .arg("-extensions") .arg("v3_ee") .arg("-out") .arg(dir.join("ee.pem"))); 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("ee.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("ee.cer"))); run(Command::new("openssl") .arg("ca") .arg("-gencrl") .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-out") .arg(dir.join("issuer.crl.pem"))); 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"), ee_der: std::fs::read(dir.join("ee.cer")).expect("read ee der"), issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"), } } #[test] fn ee_key_usage_digital_signature_only_is_accepted() { let g = generate_issuer_ca_ee_and_crl("keyUsage = critical, digitalSignature\n"); let now = time::OffsetDateTime::now_utc(); validate_ee_cert_path( &g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now, ) .expect("valid EE path"); } #[test] fn ee_key_usage_missing_is_rejected() { let g = generate_issuer_ca_ee_and_crl(""); let now = time::OffsetDateTime::now_utc(); let err = validate_ee_cert_path( &g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now, ) .unwrap_err(); assert!(matches!(err, CertPathError::KeyUsageMissing), "{err}"); } #[test] fn ee_key_usage_not_critical_is_rejected() { let g = generate_issuer_ca_ee_and_crl("keyUsage = digitalSignature\n"); let now = time::OffsetDateTime::now_utc(); let err = validate_ee_cert_path( &g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now, ) .unwrap_err(); assert!(matches!(err, CertPathError::KeyUsageNotCritical), "{err}"); } #[test] fn ee_key_usage_wrong_bits_is_rejected() { let g = generate_issuer_ca_ee_and_crl("keyUsage = critical, digitalSignature, keyEncipherment\n"); let now = time::OffsetDateTime::now_utc(); let err = validate_ee_cert_path( &g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now, ) .unwrap_err(); assert!(matches!(err, CertPathError::KeyUsageInvalidBits), "{err}"); } #[test] fn validate_ee_cert_path_with_prevalidated_issuer_covers_success_and_error_paths() { use rpki::data_model::common::BigUnsigned; use rpki::data_model::crl::RpkixCrl; use rpki::data_model::rc::ResourceCertificate; use rpki::validation::cert_path::validate_ee_cert_path_with_prevalidated_issuer; use std::collections::HashSet; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; let g = generate_issuer_ca_ee_and_crl( "keyUsage = critical, digitalSignature ", ); let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let ee = ResourceCertificate::decode_der(&g.ee_der).expect("decode ee"); let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl"); let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info) .expect("parse issuer spki"); assert!(rem.is_empty()); let now = time::OffsetDateTime::now_utc(); validate_ee_cert_path_with_prevalidated_issuer( &g.ee_der, &issuer, &issuer_spki, &issuer_crl, &HashSet::new(), Some("rsync://example.test/repo/issuer/issuer.cer"), Some("rsync://example.test/repo/issuer/issuer.crl"), now, ) .expect("prevalidated ee path ok"); let mut revoked = HashSet::new(); revoked.insert(BigUnsigned::from_biguint(&ee.tbs.serial_number).bytes_be); let err = validate_ee_cert_path_with_prevalidated_issuer( &g.ee_der, &issuer, &issuer_spki, &issuer_crl, &revoked, Some("rsync://example.test/repo/issuer/issuer.cer"), Some("rsync://example.test/repo/issuer/issuer.crl"), now, ) .unwrap_err(); assert!(matches!(err, CertPathError::EeRevoked), "{err}"); } #[test] fn validate_ee_cert_path_with_prevalidated_issuer_rejects_non_ee_and_non_ca_issuer() { use rpki::data_model::crl::RpkixCrl; use rpki::data_model::rc::ResourceCertificate; use rpki::validation::cert_path::validate_ee_cert_path_with_prevalidated_issuer; use std::collections::HashSet; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; let g = generate_issuer_ca_ee_and_crl( "keyUsage = critical, digitalSignature ", ); let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let ee = ResourceCertificate::decode_der(&g.ee_der).expect("decode ee"); let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl"); let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info) .expect("parse issuer spki"); assert!(rem.is_empty()); let now = time::OffsetDateTime::now_utc(); let err = validate_ee_cert_path_with_prevalidated_issuer( &g.issuer_ca_der, &issuer, &issuer_spki, &issuer_crl, &HashSet::new(), None, None, now, ) .unwrap_err(); assert!(matches!(err, CertPathError::EeNotEe), "{err}"); let (rem, bad_spki) = SubjectPublicKeyInfo::from_der(&ee.tbs.subject_public_key_info).expect("parse ee spki"); assert!(rem.is_empty()); let err = validate_ee_cert_path_with_prevalidated_issuer( &g.ee_der, &ee, &bad_spki, &issuer_crl, &HashSet::new(), None, None, now, ) .unwrap_err(); assert!(matches!(err, CertPathError::IssuerNotCa), "{err}"); } #[test] fn validate_ee_cert_path_with_prevalidated_issuer_rejects_mismatched_issuer_subject() { use rpki::data_model::crl::RpkixCrl; use rpki::data_model::rc::ResourceCertificate; use rpki::validation::cert_path::validate_ee_cert_path_with_prevalidated_issuer; use std::collections::HashSet; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; let g = generate_issuer_ca_ee_and_crl( "keyUsage = critical, digitalSignature ", ); let wrong_issuer_der = std::fs::read( "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", ) .expect("read wrong issuer fixture"); let wrong_issuer = ResourceCertificate::decode_der(&wrong_issuer_der).expect("decode wrong issuer"); let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl"); let (rem, wrong_spki) = SubjectPublicKeyInfo::from_der(&wrong_issuer.tbs.subject_public_key_info) .expect("parse wrong issuer spki"); assert!(rem.is_empty()); let err = validate_ee_cert_path_with_prevalidated_issuer( &g.ee_der, &wrong_issuer, &wrong_spki, &issuer_crl, &HashSet::new(), None, None, time::OffsetDateTime::now_utc(), ) .unwrap_err(); assert!( matches!(err, CertPathError::IssuerSubjectMismatch { .. }), "{err}" ); } #[test] fn validate_ee_cert_path_rejects_non_ee_and_non_ca_issuer() { use rpki::data_model::roa::RoaObject; let g = generate_issuer_ca_ee_and_crl( "keyUsage = critical, digitalSignature ", ); let roa_der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa") .expect("read roa fixture"); let roa = RoaObject::decode_der(&roa_der).expect("decode roa"); let ee_from_roa = roa.signed_object.signed_data.certificates[0] .raw_der .clone(); let now = time::OffsetDateTime::now_utc(); let err = validate_ee_cert_path( &g.issuer_ca_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, now, ) .unwrap_err(); assert!(matches!(err, CertPathError::EeNotEe), "{err}"); let err = validate_ee_cert_path(&g.ee_der, &ee_from_roa, &g.issuer_crl_der, None, None, now) .unwrap_err(); assert!(matches!(err, CertPathError::IssuerNotCa), "{err}"); } #[test] fn validate_ee_cert_path_rejects_stale_crl() { use rpki::data_model::crl::RpkixCrl; let g = generate_issuer_ca_ee_and_crl( "keyUsage = critical, digitalSignature ", ); let crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl"); let err = validate_ee_cert_path( &g.ee_der, &g.issuer_ca_der, &g.issuer_crl_der, None, None, crl.next_update.utc + time::Duration::seconds(1), ) .unwrap_err(); assert!(matches!(err, CertPathError::CrlNotValidAtTime), "{err}"); } #[test] fn validate_ee_cert_path_with_prevalidated_issuer_rejects_invalid_times() { use rpki::data_model::crl::RpkixCrl; use rpki::data_model::rc::ResourceCertificate; use rpki::validation::cert_path::validate_ee_cert_path_with_prevalidated_issuer; use std::collections::HashSet; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; let g = generate_issuer_ca_ee_and_crl( "keyUsage = critical, digitalSignature ", ); let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let ee = ResourceCertificate::decode_der(&g.ee_der).expect("decode ee"); let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl"); let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info) .expect("parse issuer spki"); assert!(rem.is_empty()); let err = validate_ee_cert_path_with_prevalidated_issuer( &g.ee_der, &issuer, &issuer_spki, &issuer_crl, &HashSet::new(), Some("rsync://example.test/repo/issuer/issuer.cer"), Some("rsync://example.test/repo/issuer/issuer.crl"), ee.tbs.validity_not_before - time::Duration::seconds(1), ) .unwrap_err(); assert!( matches!(err, CertPathError::CertificateNotValidAtTime), "{err}" ); let err = validate_ee_cert_path_with_prevalidated_issuer( &g.ee_der, &issuer, &issuer_spki, &issuer_crl, &HashSet::new(), Some("rsync://example.test/repo/issuer/issuer.cer"), Some("rsync://example.test/repo/issuer/issuer.crl"), issuer_crl.next_update.utc + time::Duration::seconds(1), ) .unwrap_err(); assert!(matches!(err, CertPathError::CrlNotValidAtTime), "{err}"); }