1 //! Autonomous System Provider Authorization
2 //!
3 //! This is still being discussed in the IETF. No RFC just yet.
4 //! See the following drafts:
5 //! https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-profile/
6 //! https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-verification/
7 
8 use std::fmt;
9 use std::str::FromStr;
10 use bcder::{decode, encode};
11 use bcder::{Captured, Mode, Oid, Tag};
12 use bcder::encode::Values;
13 use super::oid;
14 use super::cert::{Cert, ResourceCert};
15 use super::crypto::{Signer, SigningError};
16 use super::resources::{
17     AddressFamily, AsBlock, AsBlocks, AsBlocksBuilder, AsId, AsResources
18 };
19 use super::sigobj::{SignedObject, SignedObjectBuilder};
20 use super::x509::ValidationError;
21 
22 
23 //------------ Aspa ----------------------------------------------------------
24 #[derive(Clone, Debug)]
25 pub struct Aspa {
26     signed: SignedObject,
27     content: AsProviderAttestation,
28 }
29 
30 impl Aspa {
decode<S: decode::Source>( source: S, strict: bool ) -> Result<Self, S::Err>31     pub fn decode<S: decode::Source>(
32         source: S,
33         strict: bool
34     ) -> Result<Self, S::Err> {
35         let signed = SignedObject::decode(source, strict)?;
36         if signed.content_type().ne(&oid::CT_ASPA) {
37             return Err(decode::Malformed.into())
38         }
39         let content = signed.decode_content(|cons| {
40             AsProviderAttestation::take_from(cons)
41         })?;
42         Ok(Aspa { signed, content })
43     }
44 
process<F>( mut self, issuer: &ResourceCert, strict: bool, check_crl: F ) -> Result<(ResourceCert, AsProviderAttestation), ValidationError> where F: FnOnce(&Cert) -> Result<(), ValidationError>45     pub fn process<F>(
46         mut self,
47         issuer: &ResourceCert,
48         strict: bool,
49         check_crl: F
50     ) -> Result<(ResourceCert, AsProviderAttestation), ValidationError>
51     where F: FnOnce(&Cert) -> Result<(), ValidationError> {
52         let cert = self.signed.validate(issuer, strict)?;
53         check_crl(cert.as_ref())?;
54         self.content.validate(&cert)?;
55         Ok((cert, self.content))
56     }
57 
58     /// Returns a value encoder for a reference to an ASPA.
encode_ref(&self) -> impl encode::Values + '_59     pub fn encode_ref(&self) -> impl encode::Values + '_ {
60         self.signed.encode_ref()
61     }
62 
63     /// Returns a DER encoded Captured for this ASPA.
to_captured(&self) -> Captured64     pub fn to_captured(&self) -> Captured {
65         self.encode_ref().to_captured(Mode::Der)
66     }
67 
68     /// Returns a reference to the EE certificate of this ROA.
cert(&self) -> &Cert69     pub fn cert(&self) -> &Cert {
70         self.signed.cert()
71     }
72 }
73 
74 
75 //--- Deserialize and Serialize
76 
77 #[cfg(feature = "serde")]
78 impl serde::Serialize for Aspa {
serialize<S: serde::Serializer>( &self, serializer: S ) -> Result<S::Ok, S::Error>79     fn serialize<S: serde::Serializer>(
80         &self, serializer: S
81     ) -> Result<S::Ok, S::Error> {
82         let bytes = self.to_captured().into_bytes();
83         let b64 = base64::encode(&bytes);
84         b64.serialize(serializer)
85     }
86 }
87 
88 #[cfg(feature = "serde")]
89 impl<'de> serde::Deserialize<'de> for Aspa {
deserialize<D: serde::Deserializer<'de>>( deserializer: D ) -> Result<Self, D::Error>90     fn deserialize<D: serde::Deserializer<'de>>(
91         deserializer: D
92     ) -> Result<Self, D::Error> {
93         use serde::de;
94 
95         let string = String::deserialize(deserializer)?;
96         let decoded = base64::decode(&string).map_err(de::Error::custom)?;
97         let bytes = bytes::Bytes::from(decoded);
98         Aspa::decode(bytes, true).map_err(de::Error::custom)
99     }
100 }
101 
102 
103 //------------ AsProviderAttestation -----------------------------------------
104 
105 #[derive(Clone, Debug)]
106 pub struct AsProviderAttestation {
107     customer_as: AsId,
108     provider_as_set: ProviderAsSet,
109 }
110 
111 impl AsProviderAttestation {
take_from<S: decode::Source>( cons: &mut decode::Constructed<S> ) -> Result<Self, S::Err>112     fn take_from<S: decode::Source>(
113         cons: &mut decode::Constructed<S>
114     ) -> Result<Self, S::Err> {
115         // version [0] EXPLICIT INTEGER DEFAULT 0
116         cons.take_opt_constructed_if(Tag::CTX_0, |c| c.skip_u8_if(0))?;
117 
118         cons.take_sequence(|cons| {
119             let customer_as = AsId::take_from(cons)?;
120             let provider_as_set = ProviderAsSet::take_from(cons)?;
121 
122             Ok(AsProviderAttestation {
123                 customer_as,
124                 provider_as_set,
125             })
126         })
127     }
128 
validate( &mut self, cert: &ResourceCert ) -> Result<(), ValidationError>129     fn validate(
130         &mut self,
131         cert: &ResourceCert
132     ) -> Result<(), ValidationError> {
133         if !cert.as_resources().contains(&self.as_blocks()) {
134             return Err(ValidationError);
135         }
136         Ok(())
137     }
138 
as_blocks(&self) -> AsBlocks139     fn as_blocks(&self) -> AsBlocks {
140         let mut builder = AsBlocksBuilder::new();
141         let block = AsBlock::Id(self.customer_as);
142         builder.push(block);
143 
144         builder.finalize()
145     }
146 
as_resources(&self) -> AsResources147     pub fn as_resources(&self) -> AsResources {
148         AsResources::blocks(self.as_blocks())
149     }
150 
encode_ref(&self) -> impl encode::Values + '_151     pub fn encode_ref(&self) -> impl encode::Values + '_ {
152         encode::sequence((
153             // version is DEFAULT
154             self.customer_as.encode(),
155             &self.provider_as_set.0,
156         ))
157     }
158 }
159 
160 
161 //------------ ProviderAsSet -------------------------------------------------
162 
163 #[derive(Clone, Debug)]
164 pub struct ProviderAsSet(Captured);
165 
166 impl ProviderAsSet {
iter(&self) -> ProviderAsIter167     pub fn iter(&self) -> ProviderAsIter {
168         ProviderAsIter(self.0.as_ref())
169     }
170 
take_from<S: decode::Source>( cons: &mut decode::Constructed<S> ) -> Result<Self, S::Err>171     fn take_from<S: decode::Source>(
172         cons: &mut decode::Constructed<S>
173     ) -> Result<Self, S::Err> {
174         cons.take_sequence(|cons| {
175             cons.capture(|cons| {
176                 let mut last: Option<AsId> = None;
177                 let mut entries = true;
178                 while entries {
179                     if let Some(provider_as) = ProviderAs::take_opt_from(
180                         cons
181                     )? {
182                         let current_as_id = provider_as.provider();
183                         if let Some(last_as_id) = last {
184                             if last_as_id >= current_as_id {
185                                 return Err(decode::Malformed.into());
186                             }
187                         }
188                         last = Some(provider_as.provider());
189                     } else {
190                         entries = false;
191                     }
192                 }
193 
194                 Ok(())
195             })
196         }).map(ProviderAsSet)
197     }
198 }
199 
200 
201 //------------ ProviderAsIter ------------------------------------------------
202 
203 #[derive(Clone, Debug)]
204 pub struct ProviderAsIter<'a>(&'a [u8]);
205 
206 impl<'a> Iterator for ProviderAsIter<'a> {
207     type Item = ProviderAs;
208 
next(&mut self) -> Option<Self::Item>209     fn next(&mut self) -> Option<Self::Item> {
210         if self.0.is_empty() {
211             None
212         }
213         else {
214             Mode::Der.decode(&mut self.0, |cons| {
215                 ProviderAs::take_opt_from(cons)
216             }).unwrap()
217         }
218     }
219 }
220 
221 
222 //------------ AspaProvider ----------------------------------------------
223 
224 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
225 pub struct ProviderAs {
226     provider: AsId,
227     afi_limit: Option<AddressFamily>,
228 }
229 
230 impl ProviderAs {
new(provider: AsId) -> Self231     pub fn new(provider: AsId) -> Self {
232         ProviderAs { provider, afi_limit: None }
233     }
234 
new_v4(provider: AsId) -> Self235     pub fn new_v4(provider: AsId) -> Self {
236         ProviderAs { provider, afi_limit: Some(AddressFamily::Ipv4) }
237     }
238 
new_v6(provider: AsId) -> Self239     pub fn new_v6(provider: AsId) -> Self {
240         ProviderAs { provider, afi_limit: Some(AddressFamily::Ipv6) }
241     }
242 
provider(&self) -> AsId243     pub fn provider(&self) -> AsId {
244         self.provider
245     }
246 
afi_limit(&self) -> Option<AddressFamily>247     pub fn afi_limit(&self) -> Option<AddressFamily> {
248         self.afi_limit
249     }
250 
251 }
252 
253 impl ProviderAs {
254     //
255     //      providerAS     ::= SEQUENCE {
256     //          providerASID ::= ASID,
257     //          afiLimit     ::= OCTET STRING (SIZE (2)) OPTIONAL
258     //      }
259     //
260     //      ASID           ::= INTEGER
261 
262     /// Takes an optional ProviderAS from the beginning of an encoded value.
take_opt_from<S: decode::Source>( cons: &mut decode::Constructed<S> ) -> Result<Option<Self>, S::Err>263     pub fn take_opt_from<S: decode::Source>(
264         cons: &mut decode::Constructed<S>
265     ) -> Result<Option<Self>, S::Err> {
266         cons.take_opt_sequence(|cons|{
267             let provider = AsId::take_from(cons)?;
268             let afi_limit = AddressFamily::take_opt_from(cons)?;
269             Ok(ProviderAs { provider, afi_limit })
270         })
271     }
272 
273     /// Skips over a ProviderAs if it is present.
skip_opt_in<S: decode::Source>( cons: &mut decode::Constructed<S> ) -> Result<Option<()>, S::Err>274     pub fn skip_opt_in<S: decode::Source>(
275         cons: &mut decode::Constructed<S>
276     ) -> Result<Option<()>, S::Err> {
277         Self::take_opt_from(cons).map(|opt| opt.map(|_| ()))
278     }
279 
encode(self) -> impl encode::Values280     pub fn encode(self) -> impl encode::Values {
281         encode::sequence((
282             self.provider.encode(),
283             self.afi_limit.map(|v| v.encode())
284         ))
285     }
286 }
287 
288 
289 //--- FromStr
290 
291 impl FromStr for ProviderAs {
292     type Err = <AsId as FromStr>::Err;
293 
from_str(s: &str) -> Result<Self, Self::Err>294     fn from_str(s: &str) -> Result<Self, Self::Err> {
295         // Possible options:
296         //  AS#
297         //  AS#(v4)
298         //  AS#(v6)
299         if let Some(as_str) = s.strip_suffix("(v4)") {
300             Ok(ProviderAs::new_v4(AsId::from_str(as_str)?))
301         }
302         else if let Some(as_str) = s.strip_suffix("(v6)") {
303             Ok(ProviderAs::new_v6(AsId::from_str(as_str)?))
304         }
305         else {
306             Ok(ProviderAs::new(AsId::from_str(s)?))
307         }
308     }
309 }
310 
311 
312 //--- Display
313 
314 impl fmt::Display for ProviderAs {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result315     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316         match &self.afi_limit {
317             None => write!(f, "{}", self.provider),
318             Some(family) => {
319                 let fam_str = match &family {
320                     AddressFamily::Ipv4 => "v4",
321                     AddressFamily::Ipv6 => "v6",
322                 };
323                 write!(f, "{}({})", self.provider, fam_str)
324             }
325         }
326     }
327 }
328 
329 
330 //--- Deserialize and Serialize
331 
332 #[cfg(feature = "serde")]
333 impl serde::Serialize for ProviderAs {
serialize<S: serde::Serializer>( &self, serializer: S ) -> Result<S::Ok, S::Error>334     fn serialize<S: serde::Serializer>(
335         &self, serializer: S
336     ) -> Result<S::Ok, S::Error> {
337         self.to_string().serialize(serializer)
338     }
339 }
340 
341 #[cfg(feature = "serde")]
342 impl<'de> serde::Deserialize<'de> for ProviderAs {
deserialize<D: serde::Deserializer<'de>>( deserializer: D ) -> Result<Self, D::Error>343     fn deserialize<D: serde::Deserializer<'de>>(
344         deserializer: D
345     ) -> Result<Self, D::Error> {
346         use serde::de;
347 
348         let string = String::deserialize(deserializer)?;
349         ProviderAs::from_str(&string).map_err(de::Error::custom)
350     }
351 }
352 
353 
354 //------------ AspaBuilder ---------------------------------------------------
355 
356 pub struct AspaBuilder {
357     customer_as: AsId,
358     providers: Vec<ProviderAs>
359 }
360 
361 impl AspaBuilder {
new( customer_as: AsId, providers: Vec<ProviderAs> ) -> Result<Self, DuplicateProviderAs>362     pub fn new(
363         customer_as: AsId,
364         providers: Vec<ProviderAs>
365     ) -> Result<Self, DuplicateProviderAs> {
366         let mut builder = AspaBuilder {
367             customer_as,
368             providers,
369         };
370         builder.sort_and_verify_providers()?;
371         Ok(builder)
372     }
373 
empty(customer_as: AsId) -> Self374     pub fn empty(customer_as: AsId) -> Self {
375         AspaBuilder {
376             customer_as,
377             providers: vec![],
378         }
379     }
380 
add_provider( &mut self, provider: ProviderAs ) -> Result<(), DuplicateProviderAs>381     pub fn add_provider(
382         &mut self, provider: ProviderAs
383     ) -> Result<(), DuplicateProviderAs> {
384         self.providers.push(provider);
385         self.sort_and_verify_providers()
386     }
387 
sort_and_verify_providers( &mut self ) -> Result<(), DuplicateProviderAs>388     fn sort_and_verify_providers(
389         &mut self
390     ) -> Result<(), DuplicateProviderAs> {
391         // sort and verify if there are any duplicates
392         if self.providers.len() > 1 {
393             self.providers.sort_by_key(|p| p.provider());
394             let mut last = self.providers.first().unwrap().provider();
395             for i in 1..self.providers.len() {
396                 let new = self.providers.get(i).unwrap().provider();
397                 if new == last {
398                     return Err(DuplicateProviderAs);
399                 }
400                 last = new;
401             }
402         }
403         Ok(())
404     }
405 
into_attestation(self) -> AsProviderAttestation406     fn into_attestation(self) -> AsProviderAttestation {
407         let provider_as_set_captured = Captured::from_values(
408             Mode::Der,
409             encode::sequence(
410                 encode::slice(
411                     self.providers.as_slice(),
412                     |prov| prov.encode()
413                 )
414             )
415         );
416 
417         let provider_as_set = ProviderAsSet(provider_as_set_captured);
418 
419         AsProviderAttestation {
420             customer_as: self.customer_as,
421             provider_as_set,
422         }
423     }
424 
425     /// Finalizes the builder into an ASPA.
finalize<S: Signer>( self, mut sigobj: SignedObjectBuilder, signer: &S, issuer_key: &S::KeyId, ) -> Result<Aspa, SigningError<S::Error>>426     pub fn finalize<S: Signer>(
427         self,
428         mut sigobj: SignedObjectBuilder,
429         signer: &S,
430         issuer_key: &S::KeyId,
431     ) -> Result<Aspa, SigningError<S::Error>> {
432         let content = self.into_attestation();
433         sigobj.set_as_resources(content.as_resources());
434 
435         let signed = sigobj.finalize(
436             Oid(oid::CT_ASPA.0.into()),
437             content.encode_ref().to_captured(Mode::Der).into_bytes(),
438             signer,
439             issuer_key,
440         )?;
441         Ok(Aspa { signed, content })
442     }
443 }
444 
445 
446 //------------ DuplicateProviderAs -------------------------------------------
447 
448 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
449 pub struct DuplicateProviderAs;
450 
451 impl fmt::Display for DuplicateProviderAs {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result452     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
453         f.write_str("provider as set contains duplicate")
454     }
455 }
456 
457 impl std::error::Error for DuplicateProviderAs { }
458 
459 
460 //============ Test ==========================================================
461 
462 #[cfg(all(test, feature = "softkeys"))]
463 mod signer_test {
464     use std::str::FromStr;
465     use crate::uri;
466     use crate::repository::cert::{KeyUsage, Overclaim, TbsCert};
467     use crate::repository::crypto::{PublicKeyFormat, Signer};
468     use crate::repository::crypto::softsigner::OpenSslSigner;
469     use crate::repository::resources::{AsId, Prefix};
470     use crate::repository::tal::TalInfo;
471     use crate::repository::x509::Validity;
472     use super::*;
473 
474 
make_aspa( customer_as: AsId, mut providers: Vec<ProviderAs>, ) -> Aspa475     fn make_aspa(
476         customer_as: AsId,
477         mut providers: Vec<ProviderAs>,
478     ) -> Aspa {
479         let signer = OpenSslSigner::new();
480 
481         let issuer_key = signer.create_key(PublicKeyFormat::Rsa).unwrap();
482         let issuer_uri = uri::Rsync::from_str(
483             "rsync://example.com/parent/ca.cer"
484         ).unwrap();
485         let crl_uri = uri::Rsync::from_str(
486             "rsync://example.com/ca/ca.crl"
487         ).unwrap();
488         let asa_uri = uri::Rsync::from_str(
489             "rsync://example.com/ca/asa.asa"
490         ).unwrap();
491 
492         let issuer_cert = {
493             let repo_uri = uri::Rsync::from_str(
494                 "rsync://example.com/ca/"
495             ).unwrap();
496             let mft_uri = uri::Rsync::from_str(
497                 "rsync://example.com/ca/ca.mft"
498             ).unwrap();
499 
500             let pubkey = signer.get_key_info(&issuer_key).unwrap();
501 
502             let mut cert = TbsCert::new(
503                 12u64.into(),
504                 pubkey.to_subject_name(),
505                 Validity::from_secs(86400),
506                 None,
507                 pubkey,
508                 KeyUsage::Ca,
509                 Overclaim::Refuse,
510             );
511             cert.set_basic_ca(Some(true));
512             cert.set_ca_repository(Some(repo_uri));
513             cert.set_rpki_manifest(Some(mft_uri));
514             cert.build_v4_resource_blocks(|b| b.push(Prefix::new(0, 0)));
515             cert.build_v6_resource_blocks(|b| b.push(Prefix::new(0, 0)));
516             cert.build_as_resource_blocks(|b| b.push((AsId::MIN, AsId::MAX)));
517             let cert = cert.into_cert(&signer, &issuer_key).unwrap();
518 
519             cert.validate_ta(
520                 TalInfo::from_name("foo".into()).into_arc(), true
521             ).unwrap()
522 
523 
524         };
525 
526         let mut aspa = AspaBuilder::empty(customer_as);
527 
528         for provider in &providers {
529             aspa.add_provider(*provider).unwrap();
530         }
531 
532         let aspa = aspa.finalize(
533             SignedObjectBuilder::new(
534                 123_u64.into(),
535                 Validity::from_secs(86400),
536                 crl_uri,
537                 issuer_uri,
538                 asa_uri
539             ),
540             &signer,
541             &issuer_key
542         ).unwrap();
543 
544         let encoded = aspa.to_captured();
545         let decoded = Aspa::decode(encoded.as_slice(), true).unwrap();
546 
547         assert_eq!(encoded.as_slice(), decoded.to_captured().as_slice());
548 
549         let (_, attestation) = decoded.process(
550             &issuer_cert, true, |_| Ok(())
551         ).unwrap();
552 
553         assert_eq!(customer_as, attestation.customer_as);
554         let decoded_providers: Vec<_> =
555             attestation.provider_as_set.iter().collect();
556 
557         providers.sort_by_key(|p| p.provider());
558         assert_eq!(providers, decoded_providers.as_slice());
559             // Sorted vecs should match
560 
561         aspa
562     }
563 
564     #[test]
encode_aspa()565     fn encode_aspa() {
566         let customer_as: AsId = 64496.into();
567         let providers: Vec<ProviderAs> = vec![
568             ProviderAs::new_v4(64498.into()),
569             ProviderAs::new(64497.into()),
570             ProviderAs::new_v6(64499.into())
571         ];
572         make_aspa(customer_as, providers);
573     }
574 
575     #[test]
576     #[cfg(feature = "serde")]
serde_aspa()577     fn serde_aspa() {
578         let customer_as: AsId = 64496.into();
579         let providers: Vec<ProviderAs> = vec![
580             ProviderAs::new_v4(64498.into()),
581             ProviderAs::new(64497.into()),
582             ProviderAs::new_v6(64499.into())
583         ];
584         let aspa = make_aspa(customer_as, providers);
585 
586         let serialized = serde_json::to_string(&aspa).unwrap();
587         let deserialized: Aspa = serde_json::from_str(&serialized).unwrap();
588 
589         assert_eq!(
590             aspa.to_captured().into_bytes(),
591             deserialized.to_captured().into_bytes()
592         )
593     }
594 
595     #[test]
596     #[cfg(feature = "serde")]
serde_aspa_empty_providers()597     fn serde_aspa_empty_providers() {
598         let customer_as: AsId = 64496.into();
599         let providers: Vec<ProviderAs> = vec![];
600         let aspa = make_aspa(customer_as, providers);
601 
602         let serialized = serde_json::to_string(&aspa).unwrap();
603         let deserialized: Aspa = serde_json::from_str(&serialized).unwrap();
604 
605         assert_eq!(
606             aspa.to_captured().into_bytes(),
607             deserialized.to_captured().into_bytes()
608         )
609     }
610 
611     #[test]
612     #[cfg(feature = "serde")]
serde_provider_as()613     fn serde_provider_as() {
614         let providers: Vec<ProviderAs> = vec![
615             ProviderAs::new(64497.into()),
616             ProviderAs::new_v4(64498.into()),
617             ProviderAs::new_v6(64499.into())
618         ];
619 
620         let serialized = serde_json::to_string(&providers).unwrap();
621         let deserialized: Vec<_> = serde_json::from_str(&serialized).unwrap();
622 
623         assert_eq!(
624             providers,
625             deserialized
626         )
627     }
628 }
629 
630 
631 //============ Specification Documentation ===================================
632 
633 /// ASPA Specification.
634 ///
635 /// This is a documentation-only module. It summarizes the specification for
636 /// ASPAs, how they are parsed and constructed.
637 ///
638 /// A Autonomous System Provider Authorization (ASPA) is a [signed object] that
639 /// provides a means of verifying that a Customer Autonomous System holder has
640 /// authorized members of Provider set to be its upstream providers and for the
641 /// Providers to send prefixes received from the Customer Autonomous System in
642 /// all directions including providers and peers.
643 ///
644 /// which is defined as follows:
645 ///
646 /// ```txt
647 ///      ct-ASPA CONTENT-TYPE ::=
648 ///          { ASProviderAttestation IDENTIFIED BY id-ct-ASPA }
649 ///
650 ///      id-ct-ASPA OBJECT IDENTIFIER ::= { id-ct TBD }
651 ///
652 ///      ASProviderAttestation ::= SEQUENCE {
653 ///          version [0]   ASPAVersion DEFAULT v0,
654 ///          customerASID  ASID,
655 ///          providers     ProviderASSet,
656 ///      }
657 ///
658 ///      ASPAVersion    ::= INTEGER  { v0(0) }
659 ///
660 ///      ASID           ::= INTEGER
661 ///
662 ///      providerASSET  ::= SEQUENCE (SIZE(1..MAX)) OF ProviderAS }
663 ///
664 ///      providerAS     ::= SEQUENCE {
665 ///          providerASID ::= ASID,
666 ///          afiLimit     ::= OCTET STRING (SIZE (2)) OPTIONAL
667 ///      }
668 /// ```
669 ///
670 /// The _version_ must be 0. The _afiLimit, if present, MUST be
671 /// either `"\0\x01"` for IPv4 or `"\0\x02"` for IPv6.
672 ///
673 /// [signed object]: ../../sigobj/spec/index.html
674 /// [ASPA Profile draft]:  https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-profile/
675 pub mod spec {}
676