rpki/repository/
manifest.rs

1//! RPKI Manifests.
2//!
3//! Manifests list all the files that are currently published by an RPKI CA.
4//! They are defined in RFC 6486.
5//!
6//! This module defines the type [`Manifest`] that represents a decoded
7//! manifest and the type [`ManifestContent`] for the content of a validated
8//! manifest, as well as some helper types for accessing the content.
9
10use std::{borrow, fmt, ops};
11use bcder::{decode, encode};
12use bcder::{
13    BitString, Captured, Ia5String, Mode, OctetString, Oid, Tag,
14};
15use bcder::decode::{DecodeError, IntoSource, Source};
16use bcder::encode::{PrimitiveContent, Values};
17use bytes::Bytes;
18use crate::{oid, uri};
19use crate::crypto::{DigestAlgorithm, Signer, SigningError};
20#[cfg(feature = "serde")] use crate::util::base64;
21use super::cert::{Cert, ResourceCert};
22use super::error::{ValidationError, VerificationError};
23use super::sigobj::{SignedObject, SignedObjectBuilder};
24use super::x509::{Serial, Time};
25
26
27//------------ Manifest ------------------------------------------------------
28
29/// A decoded RPKI manifest.
30///
31/// This type represents a manifest decoded from a source. In order to get to
32/// the manifest’s content, you need to validate it via the `validate`
33/// method.
34#[derive(Clone, Debug)]
35pub struct Manifest {
36    signed: SignedObject,
37    content: ManifestContent,
38}
39
40impl Manifest {
41    /// Decodes a manifest from a source.
42    #[allow(clippy::redundant_closure)]
43    pub fn decode<S: IntoSource>(
44        source: S,
45        strict: bool
46    ) -> Result<Self, DecodeError<<S::Source as Source>::Error>> {
47        let signed = SignedObject::decode_if_type(
48            source, &oid::CT_RPKI_MANIFEST, strict
49        )?;
50        let content = signed.decode_content(
51            |cons| ManifestContent::take_from(cons)
52        ).map_err(DecodeError::convert)?;
53        Ok(Manifest { signed, content })
54    }
55
56    /// Validates the manifest.
57    ///
58    /// You need to pass in the certificate of the issuing CA. If validation
59    /// succeeds, the result will be the EE certificate of the manifest and
60    /// the manifest content.
61    pub fn validate(
62        self,
63        cert: &ResourceCert,
64        strict: bool,
65    ) -> Result<(ResourceCert, ManifestContent), ValidationError> {
66        self.validate_at(cert, strict, Time::now())
67    }
68
69    pub fn validate_at(
70        self,
71        cert: &ResourceCert,
72        strict: bool,
73        now: Time
74    ) -> Result<(ResourceCert, ManifestContent), ValidationError> {
75        let cert = self.signed.validate_at(cert, strict, now)?;
76        Ok((cert, self.content))
77    }
78
79    /// Returns a value encoder for a reference to the manifest.
80    pub fn encode_ref(&self) -> impl encode::Values + '_ {
81        self.signed.encode_ref()
82    }
83
84    /// Returns a DER encoded Captured for this.
85    pub fn to_captured(&self) -> Captured {
86        self.encode_ref().to_captured(Mode::Der)
87    }
88
89    /// Returns a reference to the EE certificate of this manifest.
90    pub fn cert(&self) -> &Cert {
91        self.signed.cert()
92    }
93
94    /// Returns a reference to the manifest content.
95    pub fn content(&self) -> &ManifestContent {
96        &self.content
97    }
98}
99
100
101//--- Deref, AsRef, and Borrow
102
103impl ops::Deref for Manifest {
104    type Target = ManifestContent;
105
106    fn deref(&self) -> &Self::Target {
107        &self.content
108    }
109}
110
111impl AsRef<Manifest> for Manifest {
112    fn as_ref(&self) -> &Self {
113        self
114    }
115}
116
117impl AsRef<ManifestContent> for Manifest {
118    fn as_ref(&self) -> &ManifestContent {
119        &self.content
120    }
121}
122
123impl borrow::Borrow<ManifestContent> for Manifest {
124    fn borrow(&self) -> &ManifestContent {
125        &self.content
126    }
127}
128
129
130//--- Deserialize and Serialize
131
132#[cfg(feature = "serde")]
133impl serde::Serialize for Manifest {
134    fn serialize<S: serde::Serializer>(
135        &self,
136        serializer: S
137    ) -> Result<S::Ok, S::Error> {
138        let bytes = self.to_captured().into_bytes();
139        let b64 = base64::Serde.encode(&bytes);
140        b64.serialize(serializer)
141    }
142}
143
144#[cfg(feature = "serde")]
145impl<'de> serde::Deserialize<'de> for Manifest {
146    fn deserialize<D: serde::Deserializer<'de>>(
147        deserializer: D
148    ) -> Result<Self, D::Error> {
149        use serde::de;
150
151        let s = String::deserialize(deserializer)?;
152        let decoded = base64::Serde.decode(&s).map_err(de::Error::custom)?;
153        let bytes = Bytes::from(decoded);
154        Manifest::decode(bytes, true).map_err(de::Error::custom)
155    }
156}
157
158
159//------------ ManifestContent -----------------------------------------------
160
161/// The content of an RPKI manifest.
162#[derive(Clone, Debug)]
163pub struct ManifestContent {
164    /// The number of this manifest.
165    manifest_number: Serial,
166
167    /// The time this iteration of the manifest was created.
168    this_update: Time,
169
170    /// The time the next iteration of the manifest is likely to be created.
171    next_update: Time,
172
173    /// The digest algorithm used for the file hash.
174    file_hash_alg: DigestAlgorithm,
175
176    /// The list of files.
177    ///
178    /// This contains the content of the fileList sequence, i.e, not the
179    /// outer sequence object.
180    file_list: Captured,
181
182    /// The length of the list.
183    len: usize,
184}
185
186
187/// # Creation and Conversion
188///
189impl ManifestContent {
190    pub fn new<I, FH, F, H>(
191        manifest_number: Serial,
192        this_update: Time,
193        next_update: Time,
194        file_hash_alg: DigestAlgorithm,
195        iter: I,
196    ) -> Self
197    where
198        I: IntoIterator<Item = FH>,
199        FH: AsRef<FileAndHash<F, H>>,
200        F: AsRef<[u8]>,
201        H: AsRef<[u8]>,
202    {
203        let mut len = 0;
204        let mut file_list = Captured::builder(Mode::Der);
205        for item in iter.into_iter() {
206            file_list.extend(item.as_ref().encode_ref());
207            len += 1;
208        }
209        Self {
210            manifest_number,
211            this_update,
212            next_update,
213            file_hash_alg,
214            file_list: file_list.freeze(),
215            len
216        }
217    }
218
219    pub fn into_manifest<S: Signer>(
220        self,
221        mut sigobj: SignedObjectBuilder,
222        signer: &S,
223        issuer_key: &S::KeyId,
224    ) -> Result<Manifest, SigningError<S::Error>> {
225        sigobj.set_v4_resources_inherit();
226        sigobj.set_v6_resources_inherit();
227        sigobj.set_as_resources_inherit();
228        let signed = sigobj.finalize(
229            Oid(oid::CT_RPKI_MANIFEST.0.into()),
230            self.encode_ref().to_captured(Mode::Der).into_bytes(),
231            signer,
232            issuer_key,
233        )?;
234        Ok(Manifest { signed, content: self })
235    }
236}
237
238
239/// # Data Access
240///
241impl ManifestContent {
242    /// Returns the manifest number.
243    pub fn manifest_number(&self) -> Serial {
244        self.manifest_number
245    }
246
247    /// Returns the time when this manifest was created.
248    pub fn this_update(&self) -> Time {
249        self.this_update
250    }
251
252    /// Returns the time when the next update to the manifest should appear.
253    pub fn next_update(&self) -> Time {
254        self.next_update
255    }
256
257    /// Returns the hash algorithm for the file list entries.
258    pub fn file_hash_alg(&self) -> DigestAlgorithm {
259        self.file_hash_alg
260    }
261
262    /// Returns an iterator over the file list.
263    pub fn iter(&self) -> FileListIter {
264        FileListIter(self.file_list.clone())
265    }
266
267    /// Returns an iterator over URL and hash pairs.
268    ///
269    /// The iterator assumes that all files referred to in the manifest are
270    /// relative to the given rsync URI.
271    pub fn iter_uris<'a>(
272        &'a self,
273        base: &'a uri::Rsync
274    ) -> impl Iterator<Item = (uri::Rsync, ManifestHash)> + 'a {
275        let alg = self.file_hash_alg;
276        self.iter().map(move |item| {
277            let (file, hash) = item.into_pair();
278            (
279                base.join(file.as_ref()).unwrap(),
280                ManifestHash::new(hash, alg)
281            )
282        })
283    }
284
285    /// Returns the length of the file list.
286    pub fn len(&self) -> usize {
287        self.len
288    }
289
290    /// Returns whether the file list is empty.
291    pub fn is_empty(&self) -> bool {
292        self.file_list.is_empty()
293    }
294
295    /// Returns whether the manifest is stale.
296    ///
297    /// A manifest is stale if it’s nextUpdate time has passed.
298    pub fn is_stale(&self) -> bool {
299        self.next_update < Time::now()
300    }
301}
302
303/// # Decoding and Encoding
304///
305impl ManifestContent {
306    /// Takes the content from the beginning of an encoded constructed value.
307    pub fn take_from<S: decode::Source>(
308        cons: &mut decode::Constructed<S>
309    ) -> Result<Self, DecodeError<S::Error>> {
310        cons.take_sequence(|cons| {
311            cons.take_opt_constructed_if(Tag::CTX_0, |c| c.skip_u8_if(0))?;
312            let manifest_number = Serial::take_from(cons)?;
313            let this_update = Time::take_from(cons)?;
314            let next_update = Time::take_from(cons)?;
315            let file_hash_alg = DigestAlgorithm::take_oid_from(cons)?;
316            if this_update > next_update {
317                return Err(cons.content_err(
318                    "thisUpdate after nextUpdate"
319                ));
320            }
321
322            let mut len = 0;
323            let file_list = cons.take_sequence(|cons| {
324                cons.capture(|cons| {
325                    while let Some(()) = FileAndHash::skip_opt_in(cons)? {
326                        len += 1;
327                    }
328                    Ok(())
329                })
330            })?;
331 
332            Ok(Self {
333                manifest_number,
334                this_update,
335                next_update,
336                file_hash_alg,
337                file_list,
338                len
339            })
340        })
341    }
342
343
344    /// Returns a value encoder for a reference to the content.
345    pub fn encode_ref(&self) -> impl encode::Values + '_ {
346        encode::sequence((
347            self.manifest_number.encode(),
348            self.this_update.encode_generalized_time(),
349            self.next_update.encode_generalized_time(),
350            self.file_hash_alg.encode_oid(),
351            encode::sequence(
352                &self.file_list
353            )
354        ))
355    }
356}
357
358
359//------------ FileListIter --------------------------------------------------
360
361/// An iterator over the content of a file list.
362#[derive(Clone, Debug)]
363pub struct FileListIter(Captured);
364
365impl Iterator for FileListIter {
366    type Item = FileAndHash<Bytes, Bytes>;
367
368    fn next(&mut self) -> Option<Self::Item> {
369        self.0.decode_partial(|cons| {
370            FileAndHash::take_opt_from(cons)
371        }).unwrap()
372    }
373}
374
375
376//------------ FileAndHash ---------------------------------------------------
377
378/// An entry in the manifest file list.
379///
380/// This type contains a file name and a hash over the file. Both are
381/// expressed through generic types for superior flexibility.
382#[derive(Clone, Debug)]
383pub struct FileAndHash<F, H> {
384    /// The name of a file.
385    file: F,
386
387    /// The hash over the file’s content.
388    hash: H
389}
390
391/// # Data Access
392impl<F, H> FileAndHash<F, H> {
393    /// Creates a new value.
394    pub fn new(file: F, hash: H) -> Self {
395        FileAndHash { file, hash }
396    }
397
398    /// Returns a reference to the file name.
399    pub fn file(&self) -> &F {
400        &self.file
401    }
402
403    /// Returns a reference to the hash.
404    pub fn hash(&self) -> &H {
405        &self.hash
406    }
407
408    /// Returns a pair of the file and the hash.
409    pub fn into_pair(self) -> (F, H) {
410        (self.file, self.hash)
411    }
412}
413
414
415/// # Decoding and Encoding
416///
417impl FileAndHash<Bytes, Bytes> {
418    /// Skips over an optional value in a constructed value.
419    fn skip_opt_in<S: decode::Source>(
420        cons: &mut decode::Constructed<S>
421    ) -> Result<Option<()>, DecodeError<S::Error>> {
422        cons.take_opt_sequence(|cons| {
423            let file = Ia5String::take_from(cons)?.into_bytes();
424            if let Err(err) = Self::validate_file_name(&file) {
425                return Err(cons.content_err(err)); 
426            }
427            BitString::skip_in(cons)?;
428            Ok(())
429        })
430    }
431
432    /// Takes an optional value from the beginning of a constructed value.
433    fn take_opt_from<S: decode::Source>(
434        cons: &mut decode::Constructed<S>
435    ) -> Result<Option<Self>, DecodeError<S::Error>> {
436        cons.take_opt_sequence(|cons| {
437            let file = Ia5String::take_from(cons)?.into_bytes();
438            if let Err(err) = Self::validate_file_name(&file) {
439                return Err(cons.content_err(err)); 
440            }
441            Ok(FileAndHash {
442                file,
443                hash: BitString::take_from(cons)?.octet_bytes(),
444            })
445        })
446    }
447
448    /// Check whether the file name matches RFC 9286 4.2.2:
449    /// 
450    /// Names that appear in the fileList MUST consist of one or more 
451    /// characters chosen from the set a-z, A-Z, 0-9, - (HYPHEN), or _ 
452    /// (UNDERSCORE), followed by a single . (DOT), followed by a three letter 
453    /// extension.  The extension MUST be one of those enumerated in the "RPKI 
454    /// Repository Name Schemes" registry maintained by IANA
455    fn validate_file_name(name: &[u8]) -> Result<(), &'static str> {
456        fn valid_rfc9286_character(c: u8) -> bool {
457            c == b'-' || c == b'_' || c.is_ascii_alphanumeric()
458        }
459
460        let mut n = name;
461        while let Some((c, tail)) = n.split_first() {
462            n = tail;
463            if *c == b'.' {
464                break;
465            } 
466            else if !valid_rfc9286_character(*c) {
467                return Err("manifest filename is not RFC 9286 4.2.2 compliant");
468            }
469        }
470
471        // Now you could check whether this extension matches one in the list 
472        // ["asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak"],  but that 
473        // would be brittle, so as long as it is three valid letters we will
474        // accept it. 
475        if n.len() != 3 || !n.iter().all(|c| c.is_ascii_alphabetic()) {
476            return Err("manifest extension is not RFC 9286 4.2.2 compliant");
477        }
478
479        Ok(())
480    }
481}
482
483impl<F: AsRef<[u8]>, H: AsRef<[u8]>> FileAndHash<F, H> {
484    /// Returns a value encoder for a reference.
485    pub fn encode_ref(&self) -> impl encode::Values + '_ {
486        encode::sequence((
487            OctetString::encode_slice_as(self.file.as_ref(), Tag::IA5_STRING),
488            BitString::encode_slice(self.hash.as_ref(), 0),
489        ))
490    }
491}
492
493
494//--- AsRef
495
496impl<F: AsRef<[u8]>, H: AsRef<[u8]>> AsRef<Self> for FileAndHash<F, H> {
497    fn as_ref(&self) -> &Self {
498        self
499    }
500}
501
502
503//------------ ManifestHash --------------------------------------------------
504
505/// A file hash value gained from a manifest.
506///
507/// This type knows the hash value itself plus the digest algorithm used for
508/// this hash and thus can verify objects.
509#[derive(Clone, Debug, Eq, Hash, PartialEq)]
510pub struct ManifestHash {
511    hash: Bytes,
512    algorithm: DigestAlgorithm,
513}
514
515impl ManifestHash {
516    /// Creates a new manifest hash from the hash and algorithm.
517    pub fn new(hash: Bytes, algorithm: DigestAlgorithm) -> Self {
518        Self { hash, algorithm }
519    }
520
521    /// Verifies whether an octet sequence is matched by this hash.
522    pub fn verify<T: AsRef<[u8]>>(
523        &self,
524        t: T
525    ) -> Result<(), ManifestHashMismatch> {
526        if self.hash.as_ref() != self.algorithm.digest(t.as_ref()).as_ref() {
527            Err(ManifestHashMismatch(()))
528        }
529        else {
530            Ok(())
531        }
532    }
533
534    /// Returns the digest algorithm of the hash.
535    pub fn algorithm(&self) -> DigestAlgorithm {
536        self.algorithm
537    }
538
539    /// Returns the hash value as a bytes slice.
540    pub fn as_slice(&self) -> &[u8] {
541        self.hash.as_ref()
542    }
543}
544
545
546//============ Errors ========================================================
547
548/// A manifest hash didn’t match an object’s hash.
549#[derive(Clone, Copy, Debug)]
550pub struct ManifestHashMismatch(());
551
552impl fmt::Display for ManifestHashMismatch {
553    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
554        f.write_str("manifest hash mismatch")
555    }
556}
557
558impl From<ManifestHashMismatch> for VerificationError {
559    fn from(_: ManifestHashMismatch) -> VerificationError {
560        VerificationError::new("manifest hash mismatch")
561    }
562}
563
564
565//============ Tests =========================================================
566
567#[cfg(test)]
568mod test {
569    use crate::repository::tal::TalInfo;
570    use super::*;
571
572    #[test]
573    fn decode() {
574        let talinfo = TalInfo::from_name("foo".into()).into_arc();
575        let at = Time::utc(2019, 5, 1, 0, 0, 0);
576        let issuer = Cert::decode(
577            include_bytes!("../../test-data/repository/ta.cer").as_ref()
578        ).unwrap();
579        let issuer = issuer.validate_ta_at(talinfo, false, at).unwrap();
580        let obj = Manifest::decode(
581            include_bytes!("../../test-data/repository/ta.mft").as_ref(),
582            false
583        ).unwrap();
584        obj.validate_at(&issuer, false, at).unwrap();
585        let obj = Manifest::decode(
586            include_bytes!("../../test-data/repository/ca1.mft").as_ref(),
587            false
588        ).unwrap();
589        assert!(obj.validate_at(&issuer, false, at).is_err());
590    }
591
592    #[test]
593    fn verify_manifest_hash() {
594        let alg = DigestAlgorithm::sha256();
595        let hash = ManifestHash::new(
596            Bytes::copy_from_slice(alg.digest(b"foobar").as_ref()),
597            alg
598        );
599
600        assert!(hash.verify(b"foobar").is_ok());
601        assert!(hash.verify(b"barfoo").is_err());
602    }
603
604    #[test]
605    #[cfg(feature = "serde")]
606    fn compat_de_manifest() {
607        serde_json::from_slice::<Manifest>(include_bytes!(
608            "../../test-data/repository/serde-compat/manifest.json"
609        )).unwrap();
610    }
611
612    #[test]
613    fn charset_violation() {
614        assert!(
615            Manifest::decode(
616                // This manifest is identical to ta.mft but has a non-ASCII
617                // character in the manifest filenames.
618                include_bytes!(
619                    "../../test-data/repository/ta.mft.bad-filename"
620                ).as_ref(),
621                false,
622            ).is_err()
623        );
624    }
625
626    #[test]
627    fn manifest_file_validation() {
628        fn test_name(x: &'static str) -> bool {
629            FileAndHash::validate_file_name(x.as_bytes()).is_ok()
630        } 
631
632        assert!(test_name("correct.cer"));
633        assert!(test_name("correct.ASA"));
634        assert!(!test_name("slash//es.mft"));
635        assert!(test_name("unknownextension.abc"));
636        assert!(!test_name("new\r\nlines.gbr"));
637        assert!(test_name("dashes-and_underscores.gbr"));
638        assert!(!test_name("multiple.dots.in.file.name.roa"));
639        assert!(!test_name("too_long_extension.koen"));
640    }
641}
642
643#[cfg(all(test, feature = "softkeys"))]
644mod signer_test {
645    use std::str::FromStr;
646    use crate::repository::cert::{KeyUsage, Overclaim, TbsCert};
647    use crate::crypto::PublicKeyFormat;
648    use crate::crypto::softsigner::OpenSslSigner;
649    use crate::repository::resources::{Asn, Prefix};
650    use crate::repository::tal::TalInfo;
651    use crate::repository::x509::Validity;
652    use super::*;
653
654    fn make_test_manifest() -> Manifest {
655        let signer = OpenSslSigner::new();
656        let key = signer.create_key(PublicKeyFormat::Rsa).unwrap();
657        let pubkey = signer.get_key_info(&key).unwrap();
658        let uri = uri::Rsync::from_str("rsync://example.com/m/p").unwrap();
659
660        let mut cert = TbsCert::new(
661            12u64.into(), pubkey.to_subject_name(),
662            Validity::from_secs(86400), None, pubkey, KeyUsage::Ca,
663            Overclaim::Trim
664        );
665        cert.set_basic_ca(Some(true));
666        cert.set_ca_repository(Some(uri.clone()));
667        cert.set_rpki_manifest(Some(uri.clone()));
668        cert.build_v4_resource_blocks(|b| b.push(Prefix::new(0, 0)));
669        cert.build_v6_resource_blocks(|b| b.push(Prefix::new(0, 0)));
670        cert.build_as_resource_blocks(|b| b.push((Asn::MIN, Asn::MAX)));
671        let cert = cert.into_cert(&signer, &key).unwrap();
672
673        let content = ManifestContent::new(
674            12u64.into(), Time::now(), Time::next_week(),
675            DigestAlgorithm::default(),
676            [
677                FileAndHash::new(b"file.cer".as_ref(), b"hash".as_ref()),
678                FileAndHash::new(b"file.cer".as_ref(), b"hash".as_ref()),
679            ].iter()
680        );
681
682        let manifest = content.into_manifest(
683            SignedObjectBuilder::new(
684                12u64.into(), Validity::from_secs(86400), uri.clone(),
685                uri.clone(), uri
686            ),
687            &signer, &key
688        ).unwrap();
689        let manifest = manifest.encode_ref().to_captured(Mode::Der);
690
691        let manifest = Manifest::decode(manifest.as_slice(), true).unwrap();
692        let cert = cert.validate_ta(
693            TalInfo::from_name("foo".into()).into_arc(), true
694        ).unwrap();
695        manifest.clone().validate(&cert, true).unwrap();
696
697        manifest
698    }
699
700    #[test]
701    fn encode_manifest() {
702        make_test_manifest();
703    }
704
705    #[test]
706    #[cfg(feature = "serde")]
707    fn serde_manifest() {
708        let mft = make_test_manifest();
709        let serialized = serde_json::to_string(&mft).unwrap();
710        let deser_mft: Manifest = serde_json::from_str(&serialized).unwrap();
711
712        assert_eq!(
713            mft.to_captured().into_bytes(),
714            deser_mft.to_captured().into_bytes()
715        );
716    }
717}
718