use base64::Engine; use base64::engine::general_purpose::STANDARD_NO_PAD; use rpki::data_model::resources::as_resources::Asn; use rpki::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix}; use rpki::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey, Ski}; use rpki::slurm::file::{SlurmFile, SlurmVersion}; fn sample_spki() -> Vec { vec![ 0x30, 0x13, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x02, 0x00, 0x00, ] } fn sample_ski() -> [u8; 20] { [ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x10, 0x20, 0x30, 0x40, ] } #[test] fn parses_rfc8416_v1_slurm() { let ski_hex = hex::encode(sample_ski()); let router_public_key = STANDARD_NO_PAD.encode(sample_spki()); let json = format!( r#"{{ "slurmVersion": 1, "validationOutputFilters": {{ "prefixFilters": [ {{ "prefix": "192.0.2.0/24", "asn": 64496, "comment": "drop roa" }} ], "bgpsecFilters": [ {{ "asn": 64497, "SKI": "{ski_hex}" }} ] }}, "locallyAddedAssertions": {{ "prefixAssertions": [ {{ "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }} ], "bgpsecAssertions": [ {{ "asn": 64501, "SKI": "{ski_hex}", "routerPublicKey": "{router_public_key}" }} ] }} }}"# ); let slurm = SlurmFile::from_slice(json.as_bytes()).unwrap(); assert_eq!(slurm.version(), SlurmVersion::V1); assert_eq!(slurm.validation_output_filters().prefix_filters.len(), 1); assert_eq!(slurm.validation_output_filters().bgpsec_filters.len(), 1); assert!(slurm.validation_output_filters().aspa_filters.is_empty()); assert_eq!(slurm.locally_added_assertions().prefix_assertions.len(), 1); assert_eq!(slurm.locally_added_assertions().bgpsec_assertions.len(), 1); assert!(slurm.locally_added_assertions().aspa_assertions.is_empty()); } #[test] fn parses_v2_slurm_with_aspa_extensions() { let json = r#"{ "slurmVersion": 2, "validationOutputFilters": { "prefixFilters": [], "bgpsecFilters": [], "aspaFilters": [ { "customerAsn": 64496 } ] }, "locallyAddedAssertions": { "prefixAssertions": [], "bgpsecAssertions": [], "aspaAssertions": [ { "customerAsn": 64510, "providerAsns": [64511, 64512] } ] } }"#; let slurm = SlurmFile::from_slice(json.as_bytes()).unwrap(); assert_eq!(slurm.version(), SlurmVersion::V2); assert_eq!(slurm.validation_output_filters().aspa_filters.len(), 1); assert_eq!(slurm.locally_added_assertions().aspa_assertions.len(), 1); } #[test] fn rejects_v1_file_with_aspa_members() { let json = r#"{ "slurmVersion": 1, "validationOutputFilters": { "prefixFilters": [], "bgpsecFilters": [], "aspaFilters": [] }, "locallyAddedAssertions": { "prefixAssertions": [], "bgpsecAssertions": [] } }"#; let err = SlurmFile::from_slice(json.as_bytes()).unwrap_err(); assert!(err.to_string().contains("unknown field")); } #[test] fn rejects_non_canonical_prefixes_and_unsorted_aspa_providers() { let non_canonical = r#"{ "slurmVersion": 1, "validationOutputFilters": { "prefixFilters": [ { "prefix": "192.0.2.1/24" } ], "bgpsecFilters": [] }, "locallyAddedAssertions": { "prefixAssertions": [], "bgpsecAssertions": [] } }"#; let non_canonical_err = SlurmFile::from_slice(non_canonical.as_bytes()).unwrap_err(); assert!(non_canonical_err.to_string().contains("not canonical")); let unsorted_aspa = r#"{ "slurmVersion": 2, "validationOutputFilters": { "prefixFilters": [], "bgpsecFilters": [], "aspaFilters": [] }, "locallyAddedAssertions": { "prefixAssertions": [], "bgpsecAssertions": [], "aspaAssertions": [ { "customerAsn": 64500, "providerAsns": [64502, 64501] } ] } }"#; let aspa_err = SlurmFile::from_slice(unsorted_aspa.as_bytes()).unwrap_err(); assert!(aspa_err.to_string().contains("strictly increasing")); } #[test] fn applies_filters_before_assertions_and_excludes_duplicates() { let ski = Ski::from_bytes(sample_ski()); let spki = sample_spki(); let spki_b64 = STANDARD_NO_PAD.encode(&spki); let ski_hex = hex::encode(sample_ski()); let json = format!( r#"{{ "slurmVersion": 2, "validationOutputFilters": {{ "prefixFilters": [ {{ "prefix": "192.0.2.0/24", "asn": 64496 }} ], "bgpsecFilters": [ {{ "SKI": "{ski_hex}" }} ], "aspaFilters": [ {{ "customerAsn": 64496 }} ] }}, "locallyAddedAssertions": {{ "prefixAssertions": [ {{ "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }}, {{ "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }} ], "bgpsecAssertions": [ {{ "asn": 64501, "SKI": "{ski_hex}", "routerPublicKey": "{spki_b64}" }} ], "aspaAssertions": [ {{ "customerAsn": 64510, "providerAsns": [64511, 64512] }} ] }} }}"# ); let slurm = SlurmFile::from_slice(json.as_bytes()).unwrap(); let input = vec![ Payload::RouteOrigin(RouteOrigin::new( IPAddressPrefix::new(IPAddress::from_ipv4("192.0.2.0".parse().unwrap()), 24), 24, Asn::from(64496u32), )), Payload::RouteOrigin(RouteOrigin::new( IPAddressPrefix::new(IPAddress::from_ipv4("203.0.113.0".parse().unwrap()), 24), 24, Asn::from(64497u32), )), Payload::RouterKey(RouterKey::new(ski, Asn::from(64497u32), spki.clone())), Payload::Aspa(Aspa::new(Asn::from(64496u32), vec![Asn::from(64498u32)])), ]; let output = slurm.apply(&input); assert_eq!(output.len(), 4); assert!(output.iter().any(|payload| matches!( payload, Payload::RouteOrigin(route_origin) if route_origin.prefix().address() == IPAddress::from_ipv4("203.0.113.0".parse().unwrap()) ))); assert!(output.iter().any(|payload| matches!( payload, Payload::RouteOrigin(route_origin) if route_origin.prefix().address() == IPAddress::from_ipv4("198.51.100.0".parse().unwrap()) ))); assert!(output.iter().any(|payload| matches!( payload, Payload::RouterKey(router_key) if router_key.asn() == Asn::from(64501u32) ))); assert!(output.iter().any(|payload| matches!( payload, Payload::Aspa(aspa) if aspa.customer_asn() == Asn::from(64510u32) ))); } #[test] fn merges_multiple_slurm_files_without_conflict() { let a = r#"{ "slurmVersion": 1, "validationOutputFilters": { "prefixFilters": [], "bgpsecFilters": [] }, "locallyAddedAssertions": { "prefixAssertions": [ { "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 } ], "bgpsecAssertions": [] } }"#; let b = r#"{ "slurmVersion": 2, "validationOutputFilters": { "prefixFilters": [], "bgpsecFilters": [], "aspaFilters": [ { "customerAsn": 64510 } ] }, "locallyAddedAssertions": { "prefixAssertions": [], "bgpsecAssertions": [], "aspaAssertions": [] } }"#; let merged = SlurmFile::merge_named(vec![ ( "a.slurm".to_string(), SlurmFile::from_slice(a.as_bytes()).unwrap(), ), ( "b.slurm".to_string(), SlurmFile::from_slice(b.as_bytes()).unwrap(), ), ]) .unwrap(); assert_eq!(merged.version(), SlurmVersion::V2); assert_eq!(merged.locally_added_assertions().prefix_assertions.len(), 1); assert_eq!(merged.validation_output_filters().aspa_filters.len(), 1); } #[test] fn rejects_conflicting_multiple_slurm_files() { let a = r#"{ "slurmVersion": 1, "validationOutputFilters": { "prefixFilters": [], "bgpsecFilters": [] }, "locallyAddedAssertions": { "prefixAssertions": [ { "prefix": "10.0.0.0/8", "asn": 64500, "maxPrefixLength": 24 } ], "bgpsecAssertions": [] } }"#; let b = r#"{ "slurmVersion": 1, "validationOutputFilters": { "prefixFilters": [ { "prefix": "10.0.0.0/16" } ], "bgpsecFilters": [] }, "locallyAddedAssertions": { "prefixAssertions": [], "bgpsecAssertions": [] } }"#; let err = SlurmFile::merge_named(vec![ ( "a.slurm".to_string(), SlurmFile::from_slice(a.as_bytes()).unwrap(), ), ( "b.slurm".to_string(), SlurmFile::from_slice(b.as_bytes()).unwrap(), ), ]) .unwrap_err(); assert!(err.to_string().contains("conflicting SLURM files")); }