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}