bgpkit_commons/rpki/mod.rs
1//! RPKI (Resource Public Key Infrastructure) validation and data structures.
2//!
3//! This module provides functionality for loading and validating RPKI data from multiple sources,
4//! including real-time data from Cloudflare and historical data from RIPE NCC.
5//!
6//! # Overview
7//!
8//! RPKI is a cryptographic framework used to secure internet routing by providing a way to
9//! validate the authenticity of BGP route announcements. This module implements RPKI validation
10//! using Route Origin Authorizations (ROAs) that specify which Autonomous Systems (ASes) are
11//! authorized to originate specific IP prefixes.
12//!
13//! # Data Sources
14//!
15//! ## Real-time Data (Cloudflare)
16//! - **Source**: [Cloudflare RPKI Portal](https://rpki.cloudflare.com/rpki.json)
17//! - **Format**: JSON with ROAs, ASPAs, and BGPsec keys
18//! - **Update Frequency**: Real-time
19//! - **Features**: Includes expiry timestamps for temporal validation
20//!
21//! ## Historical Data (RIPE NCC)
22//! - **Source**: [RIPE NCC FTP archives](https://ftp.ripe.net/rpki/)
23//! - **Format**: CSV files with historical RPKI states
24//! - **Use Case**: Historical analysis and research
25//! - **Date Range**: Configurable historical date
26//! - **TAL Sources**:
27//! - AFRINIC: <https://ftp.ripe.net/rpki/afrinic.tal/>
28//! - APNIC: <https://ftp.ripe.net/rpki/apnic.tal/>
29//! - ARIN: <https://ftp.ripe.net/rpki/arin.tal/>
30//! - LACNIC: <https://ftp.ripe.net/rpki/lacnic.tal/>
31//! - RIPE NCC: <https://ftp.ripe.net/rpki/ripencc.tal/>
32//!
33//! # Core Data Structures
34//!
35//! ## RpkiTrie
36//! The main data structure that stores RPKI data in a trie for efficient prefix lookups:
37//! - **Trie**: `IpnetTrie<Vec<RoaEntry>>` - Maps IP prefixes to lists of ROA entries
38//! - **ASPAs**: `Vec<CfAspaEntry>` - AS Provider Authorization records
39//! - **Date**: `Option<NaiveDate>` - Optional date for historical data
40//!
41//! ## RoaEntry
42//! Represents a Route Origin Authorization with the following fields:
43//! - `prefix: IpNet` - The IP prefix (e.g., 192.0.2.0/24)
44//! - `asn: u32` - The authorized ASN (e.g., 64496)
45//! - `max_length: u8` - Maximum allowed prefix length for more specifics
46//! - `rir: Option<Rir>` - Regional Internet Registry that issued the ROA
47//! - `not_before: Option<NaiveDateTime>` - ROA validity start time
48//! - `not_after: Option<NaiveDateTime>` - ROA validity end time (from expires field)
49//!
50//! ## Validation Results
51//! RPKI validation returns one of three states:
52//! - **Valid**: The prefix-ASN pair is explicitly authorized by a valid ROA
53//! - **Invalid**: The prefix has ROAs but none authorize the given ASN
54//! - **Unknown**: No ROAs exist for the prefix, or all ROAs are outside their validity period
55//!
56//! # Validation Process
57//!
58//! ## Standard Validation (`validate`)
59//! 1. Look up all ROAs that cover the given prefix
60//! 2. Check if any ROA authorizes the given ASN with appropriate max_length
61//! 3. Return Valid/Invalid/Unknown based on matches
62//!
63//! ## Expiry-Aware Validation (`validate_check_expiry`)
64//! 1. Look up all ROAs that cover the given prefix
65//! 2. Filter ROAs to only include those within their validity time window:
66//! - Check `not_before` ≤ check_time (if present)
67//! - Check `not_after` ≥ check_time (if present)
68//! 3. Among time-valid ROAs, check for ASN authorization
69//! 4. Return validation result:
70//! - **Valid**: Time-valid ROA found for the ASN
71//! - **Invalid**: Time-valid ROAs exist but none authorize the ASN
72//! - **Unknown**: No ROAs found, or all ROAs are outside validity period
73//!
74//! # Key Features
75//!
76//! - **Multiple ROAs per prefix**: A single prefix can have multiple valid ROAs with different ASNs
77//! - **Duplicate prevention**: ROAs with identical (prefix, asn, max_length) are automatically deduplicated
78//! - **Efficient lookup**: Fast prefix matching using a trie data structure
79//! - **Temporal validation**: Support for time-aware validation with expiry checking
80//! - **Comprehensive validation**: Full RPKI validation against stored ROAs
81//!
82//! # Usage Examples
83//!
84//! ## Loading Real-time Data (Cloudflare)
85//! ```rust,no_run
86//! use bgpkit_commons::BgpkitCommons;
87//!
88//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
89//! let mut commons = BgpkitCommons::new();
90//!
91//! // Load current RPKI data from Cloudflare
92//! commons.load_rpki(None)?;
93//!
94//! // Validate a prefix-ASN pair (standard validation)
95//! let result = commons.rpki_validate(64496, "192.0.2.0/24")?;
96//! match result {
97//! bgpkit_commons::rpki::RpkiValidation::Valid => println!("Route is RPKI valid"),
98//! bgpkit_commons::rpki::RpkiValidation::Invalid => println!("Route is RPKI invalid"),
99//! bgpkit_commons::rpki::RpkiValidation::Unknown => println!("No RPKI data for this prefix"),
100//! }
101//!
102//! // Validate with expiry checking (current time)
103//! let result = commons.rpki_validate_check_expiry(64496, "192.0.2.0/24", None)?;
104//!
105//! // Validate with expiry checking (specific time)
106//! use chrono::{DateTime, Utc};
107//! let check_time = DateTime::from_timestamp(1700000000, 0).unwrap().naive_utc();
108//! let result = commons.rpki_validate_check_expiry(64496, "192.0.2.0/24", Some(check_time))?;
109//! # Ok(())
110//! # }
111//! ```
112//!
113//! ## Loading Historical Data (RIPE)
114//! ```rust,no_run
115//! use bgpkit_commons::BgpkitCommons;
116//! use chrono::NaiveDate;
117//!
118//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
119//! let mut commons = BgpkitCommons::new();
120//!
121//! // Load RPKI data for a specific historical date
122//! let date = NaiveDate::from_ymd_opt(2023, 6, 15).unwrap();
123//! commons.load_rpki(Some(date))?;
124//!
125//! // Validate using historical data
126//! let result = commons.rpki_validate(64496, "192.0.2.0/24")?;
127//! # Ok(())
128//! # }
129//! ```
130//!
131//! ## Direct Trie Usage
132//! ```rust,no_run
133//! use bgpkit_commons::rpki::{RpkiTrie, RpkiValidation};
134//! use ipnet::IpNet;
135//!
136//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
137//! // Load from Cloudflare directly
138//! let trie = RpkiTrie::from_cloudflare()?;
139//!
140//! // Lookup all ROAs for a prefix
141//! let prefix: IpNet = "192.0.2.0/24".parse()?;
142//! let roas = trie.lookup_by_prefix(&prefix);
143//! println!("Found {} ROAs for prefix", roas.len());
144//!
145//! // Validate with expiry checking
146//! let result = trie.validate_check_expiry(&prefix, 64496, None);
147//! # Ok(())
148//! # }
149//! ```
150//!
151//! ## Handling Multiple ROAs
152//! A single prefix can have multiple ROAs with different ASNs and validity periods:
153//! ```rust,no_run
154//! use bgpkit_commons::BgpkitCommons;
155//!
156//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
157//! let mut commons = BgpkitCommons::new();
158//! commons.load_rpki(None)?;
159//!
160//! // Look up all ROAs for a prefix
161//! let roas = commons.rpki_lookup_by_prefix("192.0.2.0/24")?;
162//! for roa in roas {
163//! println!("ASN: {}, Max Length: {}, Expires: {:?}",
164//! roa.asn, roa.max_length, roa.not_after);
165//! }
166//! # Ok(())
167//! # }
168//! ```
169//!
170//! # Performance Considerations
171//!
172//! - **Trie Structure**: Uses `ipnet-trie` for O(log n) prefix lookups
173//! - **Memory Usage**: Stores all ROAs in memory for fast access
174//! - **Loading Time**: Initial load from Cloudflare takes a few seconds
175//! - **Caching**: No automatic caching - reload when fresh data is needed
176//!
177//! # Error Handling
178//!
179//! All validation methods return `Result<RpkiValidation>` and can fail due to:
180//! - Network errors when loading data
181//! - Invalid prefix format in input
182//! - RPKI data not loaded (call `load_rpki_*` methods first)
183
184mod cloudflare;
185mod ripe_historical;
186// mod rpkiviews;
187
188use chrono::{NaiveDate, NaiveDateTime, Utc};
189use ipnet::IpNet;
190use ipnet_trie::IpnetTrie;
191
192use crate::errors::{load_methods, modules};
193use crate::{BgpkitCommons, BgpkitCommonsError, LazyLoadable, Result};
194pub use cloudflare::*;
195use serde::{Deserialize, Serialize};
196use std::fmt::Display;
197use std::str::FromStr;
198
199#[derive(Clone)]
200pub struct RpkiTrie {
201 pub trie: IpnetTrie<Vec<RoaEntry>>,
202 pub aspas: Vec<CfAspaEntry>,
203 date: Option<NaiveDate>,
204}
205
206impl Default for RpkiTrie {
207 fn default() -> Self {
208 Self {
209 trie: IpnetTrie::new(),
210 aspas: vec![],
211 date: None,
212 }
213 }
214}
215
216#[derive(Clone, Debug, Serialize, Deserialize)]
217pub struct RoaEntry {
218 pub prefix: IpNet,
219 pub asn: u32,
220 pub max_length: u8,
221 pub rir: Option<Rir>,
222 pub not_before: Option<NaiveDateTime>,
223 pub not_after: Option<NaiveDateTime>,
224}
225
226#[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
227pub enum Rir {
228 AFRINIC,
229 APNIC,
230 ARIN,
231 LACNIC,
232 RIPENCC,
233}
234
235impl FromStr for Rir {
236 type Err = String;
237
238 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
239 match s.to_lowercase().as_str() {
240 "afrinic" => Ok(Rir::AFRINIC),
241 "apnic" => Ok(Rir::APNIC),
242 "arin" => Ok(Rir::ARIN),
243 "lacnic" => Ok(Rir::LACNIC),
244 "ripe" => Ok(Rir::RIPENCC),
245 _ => Err(format!("unknown RIR: {}", s)),
246 }
247 }
248}
249
250impl Display for Rir {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 match self {
253 Rir::AFRINIC => write!(f, "AFRINIC"),
254 Rir::APNIC => write!(f, "APNIC"),
255 Rir::ARIN => write!(f, "ARIN"),
256 Rir::LACNIC => write!(f, "LACNIC"),
257 Rir::RIPENCC => write!(f, "RIPENCC"),
258 }
259 }
260}
261
262impl Rir {
263 pub fn to_ripe_ftp_root_url(&self) -> String {
264 match self {
265 Rir::AFRINIC => "https://ftp.ripe.net/rpki/afrinic.tal".to_string(),
266 Rir::APNIC => "https://ftp.ripe.net/rpki/apnic.tal".to_string(),
267 Rir::ARIN => "https://ftp.ripe.net/rpki/arin.tal".to_string(),
268 Rir::LACNIC => "https://ftp.ripe.net/rpki/lacnic.tal".to_string(),
269 Rir::RIPENCC => "https://ftp.ripe.net/rpki/ripencc.tal".to_string(),
270 }
271 }
272}
273
274#[derive(Clone, Debug, PartialEq, Eq)]
275pub enum RpkiValidation {
276 Valid,
277 Invalid,
278 Unknown,
279}
280
281impl Display for RpkiValidation {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 match self {
284 RpkiValidation::Valid => write!(f, "valid"),
285 RpkiValidation::Invalid => write!(f, "invalid"),
286 RpkiValidation::Unknown => write!(f, "unknown"),
287 }
288 }
289}
290
291impl RpkiTrie {
292 pub fn new(date: Option<NaiveDate>) -> Self {
293 Self {
294 trie: IpnetTrie::new(),
295 aspas: vec![],
296 date,
297 }
298 }
299
300 /// insert an [RoaEntry]. Returns true if this is a new prefix, false if added to existing prefix.
301 /// Duplicates are avoided - ROAs with same (prefix, asn, max_length) are considered identical.
302 pub fn insert_roa(&mut self, roa: RoaEntry) -> bool {
303 match self.trie.exact_match_mut(roa.prefix) {
304 Some(existing_roas) => {
305 // Check if this ROA already exists (same prefix, asn, max_length)
306 if !existing_roas.iter().any(|existing| {
307 existing.asn == roa.asn && existing.max_length == roa.max_length
308 }) {
309 existing_roas.push(roa);
310 }
311 false
312 }
313 None => {
314 self.trie.insert(roa.prefix, vec![roa]);
315 true
316 }
317 }
318 }
319
320 /// insert multiple [RoaEntry]s
321 pub fn insert_roas(&mut self, roas: Vec<RoaEntry>) {
322 for roa in roas {
323 self.insert_roa(roa);
324 }
325 }
326
327 /// Lookup all ROAs that match a given prefix, including invalid ones
328 pub fn lookup_by_prefix(&self, prefix: &IpNet) -> Vec<RoaEntry> {
329 let mut all_matches = vec![];
330 for (p, roas) in self.trie.matches(prefix) {
331 if p.contains(prefix) {
332 for roa in roas {
333 if roa.max_length >= prefix.prefix_len() {
334 all_matches.push(roa.clone());
335 }
336 }
337 }
338 }
339 all_matches
340 }
341
342 /// Validate a prefix with an ASN
343 ///
344 /// Return values:
345 /// - `RpkiValidation::Valid` if the prefix-asn pair is valid
346 /// - `RpkiValidation::Invalid` if the prefix-asn pair is invalid
347 /// - `RpkiValidation::Unknown` if the prefix-asn pair is not found in RPKI
348 pub fn validate(&self, prefix: &IpNet, asn: u32) -> RpkiValidation {
349 let matches = self.lookup_by_prefix(prefix);
350 if matches.is_empty() {
351 return RpkiValidation::Unknown;
352 }
353
354 for roa in matches {
355 if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
356 return RpkiValidation::Valid;
357 }
358 }
359 // there are matches but none of them is valid
360 RpkiValidation::Invalid
361 }
362
363 /// Validate a prefix with an ASN, checking expiry dates
364 ///
365 /// Return values:
366 /// - `RpkiValidation::Valid` if the prefix-asn pair is valid and not expired
367 /// - `RpkiValidation::Invalid` if the prefix-asn pair is invalid (wrong ASN)
368 /// - `RpkiValidation::Unknown` if the prefix-asn pair is not found in RPKI or all matching ROAs are outside their valid time range
369 pub fn validate_check_expiry(
370 &self,
371 prefix: &IpNet,
372 asn: u32,
373 check_time: Option<NaiveDateTime>,
374 ) -> RpkiValidation {
375 let matches = self.lookup_by_prefix(prefix);
376 if matches.is_empty() {
377 return RpkiValidation::Unknown;
378 }
379
380 let check_time = check_time.unwrap_or_else(|| Utc::now().naive_utc());
381
382 let mut found_matching_asn = false;
383
384 for roa in matches {
385 if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
386 found_matching_asn = true;
387
388 // Check if ROA is within valid time period
389 let is_valid_time = {
390 if let Some(not_before) = roa.not_before {
391 if check_time < not_before {
392 false // ROA not yet valid
393 } else {
394 true
395 }
396 } else {
397 true // no not_before constraint
398 }
399 } && {
400 if let Some(not_after) = roa.not_after {
401 if check_time > not_after {
402 false // ROA expired
403 } else {
404 true
405 }
406 } else {
407 true // no not_after constraint
408 }
409 };
410
411 if is_valid_time {
412 return RpkiValidation::Valid;
413 }
414 }
415 }
416
417 // If we found matching ASN but all ROAs are outside valid time range, return Unknown
418 if found_matching_asn {
419 return RpkiValidation::Unknown;
420 }
421
422 // There are matches but none of them match the ASN
423 RpkiValidation::Invalid
424 }
425
426 pub fn reload(&mut self) -> Result<()> {
427 match self.date {
428 Some(date) => {
429 let trie = RpkiTrie::from_ripe_historical(date)?;
430 self.trie = trie.trie;
431 }
432 None => {
433 let trie = RpkiTrie::from_cloudflare()?;
434 self.trie = trie.trie;
435 }
436 }
437
438 Ok(())
439 }
440}
441
442impl LazyLoadable for RpkiTrie {
443 fn reload(&mut self) -> Result<()> {
444 self.reload()
445 }
446
447 fn is_loaded(&self) -> bool {
448 !self.trie.is_empty()
449 }
450
451 fn loading_status(&self) -> &'static str {
452 if self.is_loaded() {
453 "RPKI data loaded"
454 } else {
455 "RPKI data not loaded"
456 }
457 }
458}
459
460impl BgpkitCommons {
461 pub fn rpki_lookup_by_prefix(&self, prefix: &str) -> Result<Vec<RoaEntry>> {
462 if self.rpki_trie.is_none() {
463 return Err(BgpkitCommonsError::module_not_loaded(
464 modules::RPKI,
465 load_methods::LOAD_RPKI,
466 ));
467 }
468
469 let prefix = prefix.parse()?;
470
471 Ok(self.rpki_trie.as_ref().unwrap().lookup_by_prefix(&prefix))
472 }
473
474 pub fn rpki_validate(&self, asn: u32, prefix: &str) -> Result<RpkiValidation> {
475 if self.rpki_trie.is_none() {
476 return Err(BgpkitCommonsError::module_not_loaded(
477 modules::RPKI,
478 load_methods::LOAD_RPKI,
479 ));
480 }
481 let prefix = prefix.parse()?;
482 Ok(self.rpki_trie.as_ref().unwrap().validate(&prefix, asn))
483 }
484
485 pub fn rpki_validate_check_expiry(
486 &self,
487 asn: u32,
488 prefix: &str,
489 check_time: Option<NaiveDateTime>,
490 ) -> Result<RpkiValidation> {
491 if self.rpki_trie.is_none() {
492 return Err(BgpkitCommonsError::module_not_loaded(
493 modules::RPKI,
494 load_methods::LOAD_RPKI,
495 ));
496 }
497 let prefix = prefix.parse()?;
498 Ok(self
499 .rpki_trie
500 .as_ref()
501 .unwrap()
502 .validate_check_expiry(&prefix, asn, check_time))
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use chrono::DateTime;
510
511 #[test]
512 fn test_multiple_roas_same_prefix() {
513 let mut trie = RpkiTrie::new(None);
514
515 // Create a test prefix
516 let prefix: IpNet = "10.0.0.0/8".parse().unwrap();
517
518 // Create multiple ROAs for the same prefix with different ASNs
519 let roa1 = RoaEntry {
520 prefix,
521 asn: 64496,
522 max_length: 16,
523 rir: Some(Rir::ARIN),
524 not_before: None,
525 not_after: None,
526 };
527
528 let roa2 = RoaEntry {
529 prefix,
530 asn: 64497,
531 max_length: 24,
532 rir: Some(Rir::ARIN),
533 not_before: None,
534 not_after: None,
535 };
536
537 // Create a duplicate ROA (same prefix, asn, max_length as roa1)
538 let roa1_duplicate = RoaEntry {
539 prefix,
540 asn: 64496,
541 max_length: 16,
542 rir: Some(Rir::APNIC), // Different RIR but same (prefix, asn, max_length)
543 not_before: None,
544 not_after: None,
545 };
546
547 // Insert ROAs
548 assert!(trie.insert_roa(roa1)); // Should return true for new prefix
549 assert!(!trie.insert_roa(roa2)); // Should return false for existing prefix
550 assert!(!trie.insert_roa(roa1_duplicate)); // Should return false and not add duplicate
551
552 // Lookup should return only 2 ROAs (duplicate should be ignored)
553 let matches = trie.lookup_by_prefix(&prefix);
554 assert_eq!(matches.len(), 2);
555
556 // Check that both ASNs are present
557 let asns: std::collections::HashSet<u32> = matches.iter().map(|r| r.asn).collect();
558 assert!(asns.contains(&64496));
559 assert!(asns.contains(&64497));
560
561 // Test validation - should be valid for both ASNs
562 assert_eq!(trie.validate(&prefix, 64496), RpkiValidation::Valid);
563 assert_eq!(trie.validate(&prefix, 64497), RpkiValidation::Valid);
564 assert_eq!(trie.validate(&prefix, 64498), RpkiValidation::Invalid);
565 }
566
567 #[test]
568 fn test_validate_check_expiry() {
569 let mut trie = RpkiTrie::new(None);
570
571 // Create a test prefix
572 let prefix: IpNet = "10.0.0.0/8".parse().unwrap();
573
574 // Create test dates
575 let past = DateTime::from_timestamp(1000000000, 0).unwrap().naive_utc(); // 2001-09-09
576 let present = DateTime::from_timestamp(1700000000, 0).unwrap().naive_utc(); // 2023-11-14
577 let future = DateTime::from_timestamp(2000000000, 0).unwrap().naive_utc(); // 2033-05-18
578
579 // Test 1: ROA with no time constraints
580 let roa_no_time = RoaEntry {
581 prefix,
582 asn: 64496,
583 max_length: 16,
584 rir: Some(Rir::ARIN),
585 not_before: None,
586 not_after: None,
587 };
588 trie.insert_roa(roa_no_time.clone());
589
590 // Should be valid at any time
591 assert_eq!(
592 trie.validate_check_expiry(&prefix, 64496, Some(past)),
593 RpkiValidation::Valid
594 );
595 assert_eq!(
596 trie.validate_check_expiry(&prefix, 64496, Some(present)),
597 RpkiValidation::Valid
598 );
599 assert_eq!(
600 trie.validate_check_expiry(&prefix, 64496, Some(future)),
601 RpkiValidation::Valid
602 );
603 assert_eq!(
604 trie.validate_check_expiry(&prefix, 64496, None),
605 RpkiValidation::Valid
606 );
607
608 // Test 2: ROA that's expired
609 let expired_roa = RoaEntry {
610 prefix,
611 asn: 64497,
612 max_length: 16,
613 rir: Some(Rir::ARIN),
614 not_before: None,
615 not_after: Some(past),
616 };
617 trie.insert_roa(expired_roa);
618
619 // Should be unknown after expiry (ROA exists but is outside valid time range)
620 assert_eq!(
621 trie.validate_check_expiry(&prefix, 64497, Some(present)),
622 RpkiValidation::Unknown
623 );
624 assert_eq!(
625 trie.validate_check_expiry(&prefix, 64497, None),
626 RpkiValidation::Unknown
627 );
628
629 // Test 3: ROA that's not yet valid
630 let future_roa = RoaEntry {
631 prefix,
632 asn: 64498,
633 max_length: 16,
634 rir: Some(Rir::ARIN),
635 not_before: Some(future),
636 not_after: None,
637 };
638 trie.insert_roa(future_roa);
639
640 // Should be unknown before validity period (ROA exists but is outside valid time range)
641 assert_eq!(
642 trie.validate_check_expiry(&prefix, 64498, Some(present)),
643 RpkiValidation::Unknown
644 );
645 assert_eq!(
646 trie.validate_check_expiry(&prefix, 64498, None),
647 RpkiValidation::Unknown
648 );
649
650 // Test 4: ROA with valid time window
651 let windowed_roa = RoaEntry {
652 prefix,
653 asn: 64499,
654 max_length: 16,
655 rir: Some(Rir::ARIN),
656 not_before: Some(past),
657 not_after: Some(future),
658 };
659 trie.insert_roa(windowed_roa);
660
661 // Should be valid within window
662 assert_eq!(
663 trie.validate_check_expiry(&prefix, 64499, Some(present)),
664 RpkiValidation::Valid
665 );
666 // Should be unknown outside window (ROA exists but is outside valid time range)
667 assert_eq!(
668 trie.validate_check_expiry(
669 &prefix,
670 64499,
671 Some(DateTime::from_timestamp(900000000, 0).unwrap().naive_utc())
672 ),
673 RpkiValidation::Unknown
674 );
675 assert_eq!(
676 trie.validate_check_expiry(
677 &prefix,
678 64499,
679 Some(DateTime::from_timestamp(2100000000, 0).unwrap().naive_utc())
680 ),
681 RpkiValidation::Unknown
682 );
683
684 // Test 5: Ensure Invalid is still returned for wrong ASN
685 assert_eq!(
686 trie.validate_check_expiry(&prefix, 99999, Some(present)),
687 RpkiValidation::Invalid
688 );
689 }
690}