use std::process::Command; use rpki::data_model::rc::{ResourceCertificate, ResourceCertKind}; use rpki::data_model::router_cert::{ BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError, BgpsecRouterCertificatePathError, BgpsecRouterCertificateProfileError, }; 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, router_der: Vec, issuer_crl_der: Vec, wrong_issuer_der: Vec, } fn generate_router_cert_with_variant( key_spec: &str, include_eku: bool, extra_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 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_router ] keyUsage = critical, digitalSignature {eku_line} 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 sbgp-autonomousSysNum = critical, AS:64496 {eku_line} {extra_ext} [ crl_ext ] authorityKeyIdentifier = keyid:always "#, dir = dir.display(), eku_line = if include_eku { "extendedKeyUsage = 1.3.6.1.5.5.7.3.30" } else { "" }, extra_ext = extra_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"))); match key_spec { "ec-p256" => { run(Command::new("openssl") .arg("ecparam") .arg("-name") .arg("prime256v1") .arg("-genkey") .arg("-noout") .arg("-out") .arg(dir.join("router.key"))); } "ec-p384" => { run(Command::new("openssl") .arg("ecparam") .arg("-name") .arg("secp384r1") .arg("-genkey") .arg("-noout") .arg("-out") .arg(dir.join("router.key"))); } "rsa" => { run(Command::new("openssl") .arg("genrsa") .arg("-out") .arg(dir.join("router.key")) .arg("2048")); } other => panic!("unsupported key_spec {other}"), } run(Command::new("openssl") .arg("req") .arg("-new") .arg("-key") .arg(dir.join("router.key")) .arg("-subj") .arg("/CN=ROUTER-0000FC10/serialNumber=01020304") .arg("-out") .arg(dir.join("router.csr"))); run(Command::new("openssl") .arg("ca") .arg("-batch") .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-in") .arg(dir.join("router.csr")) .arg("-extensions") .arg("v3_router") .arg("-out") .arg(dir.join("router.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("router.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("router.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"))); // Wrong issuer for path failure. run(Command::new("openssl") .arg("genrsa") .arg("-out") .arg(dir.join("other.key")) .arg("2048")); run(Command::new("openssl") .arg("req") .arg("-new") .arg("-x509") .arg("-sha256") .arg("-days") .arg("365") .arg("-key") .arg(dir.join("other.key")) .arg("-subj") .arg("/CN=Other Issuer") .arg("-out") .arg(dir.join("other.pem"))); run(Command::new("openssl") .arg("x509") .arg("-in") .arg(dir.join("other.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("other.cer"))); Generated { issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"), router_der: std::fs::read(dir.join("router.cer")).expect("read router der"), issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"), wrong_issuer_der: std::fs::read(dir.join("other.cer")).expect("read other issuer der"), } } #[test] fn decode_bgpsec_router_certificate_fixture_smoke() { let g = generate_router_cert_with_variant("ec-p256", true, ""); let cert = BgpsecRouterCertificate::decode_der(&g.router_der).expect("decode router cert"); assert_eq!(cert.resource_cert.kind, ResourceCertKind::Ee); assert_eq!(cert.asns, vec![64496]); assert_eq!(cert.subject_key_identifier.len(), 20); assert_eq!(cert.spki_der[0], 0x30); } #[test] fn router_certificate_profile_rejects_missing_eku() { let g = generate_router_cert_with_variant("ec-p256", false, ""); let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err(); assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku | BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage)), "{err}"); } #[test] fn router_certificate_profile_rejects_sia_and_ip_resources_and_ranges() { let g = generate_router_cert_with_variant( "ec-p256", true, "subjectInfoAccess = caRepository;URI:rsync://example.test/repo/router/\n", ); let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err(); assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::SubjectInfoAccessPresent)), "{err}"); let g = generate_router_cert_with_variant( "ec-p256", true, "sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8\n", ); let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err(); assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::IpResourcesPresent)), "{err}"); let g = generate_router_cert_with_variant( "ec-p256", true, "sbgp-autonomousSysNum = critical, AS:64496-64500\n", ); let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err(); assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::AsResourcesRangeNotAllowed)), "{err}"); } #[test] fn router_certificate_profile_rejects_wrong_spki_algorithm_or_curve() { let g = generate_router_cert_with_variant("rsa", true, ""); let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err(); assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::SpkiAlgorithmNotEcPublicKey)), "{err}"); let g = generate_router_cert_with_variant("ec-p384", true, ""); let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err(); assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::SpkiWrongCurve | BgpsecRouterCertificateProfileError::SpkiEcPointNotUncompressedP256)), "{err}"); } #[test] fn router_certificate_path_validation_accepts_valid_and_rejects_wrong_issuer() { use rpki::data_model::common::BigUnsigned; use rpki::data_model::crl::RpkixCrl; use std::collections::HashSet; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; let g = generate_router_cert_with_variant("ec-p256", true, ""); let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let wrong_issuer = ResourceCertificate::decode_der(&g.wrong_issuer_der).expect("decode wrong issuer"); 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("issuer spki"); assert!(rem.is_empty()); let (rem, wrong_spki) = SubjectPublicKeyInfo::from_der(&wrong_issuer.tbs.subject_public_key_info).expect("wrong issuer spki"); assert!(rem.is_empty()); let now = time::OffsetDateTime::now_utc(); let cert = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer( &g.router_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("router path valid"); assert_eq!(cert.asns, vec![64496]); let err = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer( &g.router_der, &wrong_issuer, &wrong_spki, &issuer_crl, &HashSet::new(), None, None, now, ).unwrap_err(); assert!(matches!(err, BgpsecRouterCertificatePathError::CertPath(_)), "{err}"); let rc = ResourceCertificate::decode_der(&g.router_der).expect("decode router rc"); let mut revoked = HashSet::new(); revoked.insert(BigUnsigned::from_biguint(&rc.tbs.serial_number).bytes_be); let err = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer( &g.router_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, BgpsecRouterCertificatePathError::CertPath(_)), "{err}"); }