Skip to main content

tuf/
repository.rs

1//! Interfaces for interacting with different types of TUF repositories.
2
3use crate::crypto::{self, HashAlgorithm, HashValue};
4use crate::metadata::{
5    Metadata, MetadataPath, MetadataVersion, RawSignedMetadata, TargetDescription, TargetPath,
6};
7use crate::pouf::Pouf;
8use crate::util::SafeAsyncRead;
9use crate::{Error, Result};
10
11use futures_io::AsyncRead;
12use futures_util::future::BoxFuture;
13use futures_util::io::AsyncReadExt;
14use std::marker::PhantomData;
15use std::sync::Arc;
16
17mod file_system;
18pub use self::file_system::{
19    FileSystemBatchUpdate, FileSystemRepository, FileSystemRepositoryBuilder,
20};
21
22#[cfg(feature = "hyper")]
23mod http;
24
25#[cfg(feature = "hyper")]
26pub use self::http::{HttpRepository, HttpRepositoryBuilder};
27
28mod ephemeral;
29pub use self::ephemeral::{EphemeralBatchUpdate, EphemeralRepository};
30
31#[cfg(test)]
32mod error_repo;
33#[cfg(test)]
34pub(crate) use self::error_repo::ErrorRepository;
35
36#[cfg(test)]
37mod track_repo;
38#[cfg(test)]
39pub(crate) use self::track_repo::{Track, TrackRepository};
40
41/// A readable TUF repository.
42pub trait RepositoryProvider<D>
43where
44    D: Pouf,
45{
46    /// Fetch signed metadata identified by `meta_path`, `version`, and
47    /// [`D::extension()`][extension].
48    ///
49    /// Implementations may ignore `max_length` and `hash_data` as [`Client`][Client] will verify
50    /// these constraints itself. However, it may be more efficient for an implementation to detect
51    /// invalid metadata and fail the fetch operation before streaming all of the bytes of the
52    /// metadata.
53    ///
54    /// [extension]: crate::pouf::Pouf::extension
55    /// [Client]: crate::client::Client
56    fn fetch_metadata<'a>(
57        &'a self,
58        meta_path: &MetadataPath,
59        version: MetadataVersion,
60    ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>>;
61
62    /// Fetch the given target.
63    ///
64    /// Implementations may ignore the `length` and `hashes` fields in `target_description` as
65    /// [`Client`][Client] will verify these constraints itself. However, it may be more efficient
66    /// for an implementation to detect invalid targets and fail the fetch operation before
67    /// streaming all of the bytes.
68    ///
69    /// [Client]: crate::client::Client
70    fn fetch_target<'a>(
71        &'a self,
72        target_path: &TargetPath,
73    ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>>;
74}
75
76/// Test helper to help read a metadata file from a repository into a string.
77#[cfg(test)]
78pub(crate) async fn fetch_metadata_to_string<D, R>(
79    repo: &R,
80    meta_path: &MetadataPath,
81    version: MetadataVersion,
82) -> Result<String>
83where
84    D: Pouf,
85    R: RepositoryProvider<D>,
86{
87    let mut reader = repo.fetch_metadata(meta_path, version).await?;
88    let mut buf = String::new();
89    reader.read_to_string(&mut buf).await.unwrap();
90    Ok(buf)
91}
92
93/// Test helper to help read a target file from a repository into a string.
94#[cfg(test)]
95pub(crate) async fn fetch_target_to_string<D, R>(
96    repo: &R,
97    target_path: &TargetPath,
98) -> Result<String>
99where
100    D: Pouf,
101    R: RepositoryProvider<D>,
102{
103    let mut reader = repo.fetch_target(target_path).await?;
104    let mut buf = String::new();
105    reader.read_to_string(&mut buf).await.unwrap();
106    Ok(buf)
107}
108
109/// A writable TUF repository. Most implementors of this trait should also implement
110/// `RepositoryProvider`.
111pub trait RepositoryStorage<D>
112where
113    D: Pouf,
114{
115    /// Store the provided `metadata` in a location identified by `meta_path`, `version`, and
116    /// [`D::extension()`][extension], overwriting any existing metadata at that location.
117    ///
118    /// [extension]: crate::pouf::Pouf::extension
119    fn store_metadata<'a>(
120        &'a self,
121        meta_path: &MetadataPath,
122        version: MetadataVersion,
123        metadata: &'a mut (dyn AsyncRead + Send + Unpin),
124    ) -> BoxFuture<'a, Result<()>>;
125
126    /// Store the provided `target` in a location identified by `target_path`, overwriting any
127    /// existing target at that location.
128    fn store_target<'a>(
129        &'a self,
130        target_path: &TargetPath,
131        target: &'a mut (dyn AsyncRead + Send + Unpin),
132    ) -> BoxFuture<'a, Result<()>>;
133}
134
135/// A subtrait of both RepositoryStorage and RepositoryProvider. This is useful to create
136/// trait objects that implement both traits.
137pub trait RepositoryStorageProvider<D>: RepositoryStorage<D> + RepositoryProvider<D>
138where
139    D: Pouf,
140{
141}
142
143impl<D, T> RepositoryStorageProvider<D> for T
144where
145    D: Pouf,
146    T: RepositoryStorage<D> + RepositoryProvider<D>,
147{
148}
149
150macro_rules! impl_provider {
151    (
152        <$($desc:tt)+
153    ) => {
154        impl<$($desc)+ {
155            fn fetch_metadata<'a>(
156                &'a self,
157                meta_path: &MetadataPath,
158                version: MetadataVersion,
159            ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
160                (**self).fetch_metadata(meta_path, version)
161            }
162
163            fn fetch_target<'a>(
164                &'a self,
165                target_path: &TargetPath,
166            ) -> BoxFuture<'a, Result<Box<dyn AsyncRead + Send + Unpin + 'a>>> {
167                (**self).fetch_target(target_path)
168            }
169        }
170    };
171}
172
173impl_provider!(<D: Pouf, T: RepositoryProvider<D> + ?Sized> RepositoryProvider<D> for &T);
174impl_provider!(<D: Pouf, T: RepositoryProvider<D> + ?Sized> RepositoryProvider<D> for &mut T);
175impl_provider!(<D: Pouf, T: RepositoryProvider<D> + ?Sized> RepositoryProvider<D> for Box<T>);
176impl_provider!(<D: Pouf, T: RepositoryProvider<D> + ?Sized> RepositoryProvider<D> for Arc<T>);
177
178macro_rules! impl_storage {
179    (
180        <$($desc:tt)+
181    ) => {
182        impl<$($desc)+ {
183            fn store_metadata<'a>(
184                &'a self,
185                meta_path: &MetadataPath,
186                version: MetadataVersion,
187                metadata: &'a mut (dyn AsyncRead + Send + Unpin),
188            ) -> BoxFuture<'a, Result<()>> {
189                (**self).store_metadata(meta_path, version, metadata)
190            }
191
192            fn store_target<'a>(
193                &'a self,
194                target_path: &TargetPath,
195                target: &'a mut (dyn AsyncRead + Send + Unpin),
196            ) -> BoxFuture<'a, Result<()>> {
197                (**self).store_target(target_path, target)
198            }
199        }
200    };
201}
202
203impl_storage!(<D: Pouf, T: RepositoryStorage<D> + ?Sized> RepositoryStorage<D> for &T);
204impl_storage!(<D: Pouf, T: RepositoryStorage<D> + ?Sized> RepositoryStorage<D> for &mut T);
205impl_storage!(<D: Pouf, T: RepositoryStorage<D> + ?Sized> RepositoryStorage<D> for Box<T>);
206impl_storage!(<D: Pouf, T: RepositoryStorage<D> + ?Sized> RepositoryStorage<D> for Arc<T>);
207
208/// A wrapper around an implementation of [`RepositoryProvider`] and/or [`RepositoryStorage`] tied
209/// to a specific [Pouf] that will enforce provided length limits and hash checks.
210#[derive(Debug, Clone)]
211pub(crate) struct Repository<R, D> {
212    repository: R,
213    _pouf: PhantomData<D>,
214}
215
216impl<R, D> Repository<R, D> {
217    /// Creates a new [`Repository`] wrapping `repository`.
218    pub(crate) fn new(repository: R) -> Self {
219        Self {
220            repository,
221            _pouf: PhantomData,
222        }
223    }
224
225    /// Perform a sanity check that `M`, `Role`, and `MetadataPath` all describe the same entity.
226    fn check<M>(meta_path: &MetadataPath) -> Result<()>
227    where
228        M: Metadata,
229    {
230        if !M::ROLE.fuzzy_matches_path(meta_path) {
231            return Err(Error::IllegalArgument(format!(
232                "Role {} does not match path {:?}",
233                M::ROLE,
234                meta_path
235            )));
236        }
237
238        Ok(())
239    }
240
241    pub(crate) fn into_inner(self) -> R {
242        self.repository
243    }
244
245    pub(crate) fn as_inner(&self) -> &R {
246        &self.repository
247    }
248
249    pub(crate) fn as_inner_mut(&mut self) -> &mut R {
250        &mut self.repository
251    }
252}
253
254impl<R, D> Repository<R, D>
255where
256    R: RepositoryProvider<D>,
257    D: Pouf,
258{
259    /// Fetch metadata identified by `meta_path`, `version`, and [`D::extension()`][extension].
260    ///
261    /// If `max_length` is provided, this method will return an error if the metadata exceeds
262    /// `max_length` bytes. If `hash_data` is provided, this method will return and error if the
263    /// hashed bytes of the metadata do not match `hash_data`.
264    ///
265    /// [extension]: crate::pouf::Pouf::extension
266    pub(crate) async fn fetch_metadata<'a, M>(
267        &'a self,
268        meta_path: &'a MetadataPath,
269        version: MetadataVersion,
270        max_length: Option<usize>,
271        hashes: Vec<(&'static HashAlgorithm, HashValue)>,
272    ) -> Result<RawSignedMetadata<D, M>>
273    where
274        M: Metadata,
275    {
276        Self::check::<M>(meta_path)?;
277
278        // Fetch the metadata, verifying max_length and hashes (if provided), as
279        // the repository implementation should only be trusted to use those as
280        // hints to fail early.
281        let mut reader = self
282            .repository
283            .fetch_metadata(meta_path, version)
284            .await?
285            .check_length_and_hash(max_length.unwrap_or(usize::MAX) as u64, hashes)?;
286
287        let mut buf = Vec::new();
288        reader.read_to_end(&mut buf).await?;
289
290        Ok(RawSignedMetadata::new(buf))
291    }
292
293    /// Fetch the target identified by `target_path` through the returned `AsyncRead`, verifying
294    /// that the target matches the preferred hash specified in `target_description` and that it is
295    /// the expected length. Such verification errors will be provided by a read failure on the
296    /// provided `AsyncRead`.
297    ///
298    /// It is **critical** that none of the bytes from the returned `AsyncRead` are used until it
299    /// has been fully consumed as the data is untrusted.
300    pub(crate) async fn fetch_target(
301        &self,
302        consistent_snapshot: bool,
303        target_path: &TargetPath,
304        target_description: TargetDescription,
305    ) -> Result<impl AsyncRead + Send + Unpin + '_> {
306        // https://theupdateframework.github.io/specification/v1.0.26/#fetch-target 5.7.3:
307        //
308        // [...] download the target (up to the number of bytes specified in the targets metadata),
309        // and verify that its hashes match the targets metadata.
310        let length = target_description.length();
311        let hashes = crypto::retain_supported_hashes(target_description.hashes());
312        if hashes.is_empty() {
313            return Err(Error::NoSupportedHashAlgorithm);
314        }
315
316        // https://theupdateframework.github.io/specification/v1.0.26/#fetch-target 5.7.3:
317        //
318        // [...] If consistent snapshots are not used (see § 6.2 Consistent snapshots), then the
319        // filename used to download the target file is of the fixed form FILENAME.EXT (e.g.,
320        // foobar.tar.gz). Otherwise, the filename is of the form HASH.FILENAME.EXT [...]
321        let target = if consistent_snapshot {
322            let mut hashes = hashes.iter();
323            loop {
324                if let Some((_, hash)) = hashes.next() {
325                    let target_path = target_path.with_hash_prefix(hash)?;
326                    match self.repository.fetch_target(&target_path).await {
327                        Ok(target) => break target,
328                        Err(Error::TargetNotFound(_)) => {}
329                        Err(err) => return Err(err),
330                    }
331                } else {
332                    return Err(Error::TargetNotFound(target_path.clone()));
333                }
334            }
335        } else {
336            self.repository.fetch_target(target_path).await?
337        };
338
339        target.check_length_and_hash(length, hashes)
340    }
341}
342
343impl<R, D> Repository<R, D>
344where
345    R: RepositoryStorage<D>,
346    D: Pouf,
347{
348    /// Store the provided `metadata` in a location identified by `meta_path`, `version`, and
349    /// [`D::extension()`][extension], overwriting any existing metadata at that location.
350    ///
351    /// [extension]: crate::pouf::Pouf::extension
352    pub async fn store_metadata<'a, M>(
353        &'a mut self,
354        path: &MetadataPath,
355        version: MetadataVersion,
356        metadata: &'a RawSignedMetadata<D, M>,
357    ) -> Result<()>
358    where
359        M: Metadata + Sync,
360    {
361        Self::check::<M>(path)?;
362
363        self.repository
364            .store_metadata(path, version, &mut metadata.as_bytes())
365            .await
366    }
367
368    /// Store the provided `target` in a location identified by `target_path`.
369    pub async fn store_target<'a>(
370        &'a mut self,
371        target_path: &TargetPath,
372        target: &'a mut (dyn AsyncRead + Send + Unpin + 'a),
373    ) -> Result<()> {
374        self.repository.store_target(target_path, target).await
375    }
376}
377
378#[cfg(test)]
379mod test {
380    use super::*;
381    use crate::metadata::{MetadataPath, MetadataVersion, RootMetadata, SnapshotMetadata};
382    use crate::pouf::Pouf1;
383    use crate::repository::EphemeralRepository;
384    use assert_matches::assert_matches;
385    use futures_executor::block_on;
386
387    #[test]
388    fn repository_forwards_not_found_error() {
389        block_on(async {
390            let repo = Repository::<_, Pouf1>::new(EphemeralRepository::new());
391
392            assert_matches!(
393                repo.fetch_metadata::<RootMetadata>(
394                    &MetadataPath::root(),
395                    MetadataVersion::None,
396                    None,
397                    vec![],
398                )
399                .await,
400                Err(Error::MetadataNotFound { path, version })
401                if path == MetadataPath::root() && version == MetadataVersion::None
402            );
403        });
404    }
405
406    #[test]
407    fn repository_rejects_mismatched_path() {
408        block_on(async {
409            let mut repo = Repository::<_, Pouf1>::new(EphemeralRepository::new());
410            let fake_metadata = RawSignedMetadata::<Pouf1, RootMetadata>::new(vec![]);
411
412            repo.store_metadata(&MetadataPath::root(), MetadataVersion::None, &fake_metadata)
413                .await
414                .unwrap();
415
416            assert_matches!(
417                repo.store_metadata(
418                    &MetadataPath::snapshot(),
419                    MetadataVersion::None,
420                    &fake_metadata,
421                )
422                .await,
423                Err(Error::IllegalArgument(_))
424            );
425
426            assert_matches!(
427                repo.fetch_metadata::<SnapshotMetadata>(
428                    &MetadataPath::root(),
429                    MetadataVersion::None,
430                    None,
431                    vec![],
432                )
433                .await,
434                Err(Error::IllegalArgument(_))
435            );
436        });
437    }
438
439    #[test]
440    fn repository_verifies_metadata_hash() {
441        block_on(async {
442            let path = MetadataPath::root();
443            let version = MetadataVersion::None;
444            let data: &[u8] = b"valid metadata";
445            let _metadata = RawSignedMetadata::<Pouf1, RootMetadata>::new(data.to_vec());
446            let data_hash = crypto::calculate_hash(data, &HashAlgorithm::Sha256);
447
448            let repo = EphemeralRepository::new();
449            repo.store_metadata(&path, version, &mut &*data)
450                .await
451                .unwrap();
452
453            let client = Repository::<_, Pouf1>::new(repo);
454
455            assert_matches!(
456                client
457                    .fetch_metadata::<RootMetadata>(
458                        &path,
459                        version,
460                        None,
461                        vec![(&HashAlgorithm::Sha256, data_hash)],
462                    )
463                    .await,
464                Ok(_metadata)
465            );
466        })
467    }
468
469    #[test]
470    fn repository_rejects_corrupt_metadata() {
471        block_on(async {
472            let path = MetadataPath::root();
473            let version = MetadataVersion::None;
474            let data: &[u8] = b"corrupt metadata";
475
476            let repo = EphemeralRepository::new();
477            repo.store_metadata(&path, version, &mut &*data)
478                .await
479                .unwrap();
480
481            let client = Repository::<_, Pouf1>::new(repo);
482
483            assert_matches!(
484                client
485                    .fetch_metadata::<RootMetadata>(
486                        &path,
487                        version,
488                        None,
489                        vec![(&HashAlgorithm::Sha256, HashValue::new(vec![]))],
490                    )
491                    .await,
492                Err(_)
493            );
494        })
495    }
496
497    #[test]
498    fn repository_verifies_metadata_size() {
499        block_on(async {
500            let path = MetadataPath::root();
501            let version = MetadataVersion::None;
502            let data: &[u8] = b"reasonably sized metadata";
503            let _metadata = RawSignedMetadata::<Pouf1, RootMetadata>::new(data.to_vec());
504
505            let repo = EphemeralRepository::new();
506            repo.store_metadata(&path, version, &mut &*data)
507                .await
508                .unwrap();
509
510            let client = Repository::<_, Pouf1>::new(repo);
511
512            assert_matches!(
513                client
514                    .fetch_metadata::<RootMetadata>(&path, version, Some(100), vec![])
515                    .await,
516                Ok(_metadata)
517            );
518        })
519    }
520
521    #[test]
522    fn repository_rejects_oversized_metadata() {
523        block_on(async {
524            let path = MetadataPath::root();
525            let version = MetadataVersion::None;
526            let data: &[u8] = b"very big metadata";
527
528            let repo = EphemeralRepository::new();
529            repo.store_metadata(&path, version, &mut &*data)
530                .await
531                .unwrap();
532
533            let client = Repository::<_, Pouf1>::new(repo);
534
535            assert_matches!(
536                client
537                    .fetch_metadata::<RootMetadata>(&path, version, Some(4), vec![])
538                    .await,
539                Err(_)
540            );
541        })
542    }
543
544    #[test]
545    fn repository_rejects_corrupt_targets() {
546        block_on(async {
547            let repo = EphemeralRepository::new();
548            let mut client = Repository::<_, Pouf1>::new(repo);
549
550            let data: &[u8] = b"like tears in the rain";
551            let target_description =
552                TargetDescription::from_slice(data, &[HashAlgorithm::Sha256]).unwrap();
553            let path = TargetPath::new("batty").unwrap();
554            client.store_target(&path, &mut &*data).await.unwrap();
555
556            let mut read = client
557                .fetch_target(false, &path, target_description.clone())
558                .await
559                .unwrap();
560            let mut buf = Vec::new();
561            read.read_to_end(&mut buf).await.unwrap();
562            assert_eq!(buf.as_slice(), data);
563            drop(read);
564
565            let bad_data: &[u8] = b"you're in a desert";
566            client.store_target(&path, &mut &*bad_data).await.unwrap();
567            let mut read = client
568                .fetch_target(false, &path, target_description)
569                .await
570                .unwrap();
571            assert!(read.read_to_end(&mut buf).await.is_err());
572        })
573    }
574
575    #[test]
576    fn repository_takes_trait_objects() {
577        block_on(async {
578            let repo: Box<dyn RepositoryStorageProvider<Pouf1>> =
579                Box::new(EphemeralRepository::new());
580            let mut client = Repository::<_, Pouf1>::new(repo);
581
582            let data: &[u8] = b"like tears in the rain";
583            let target_description =
584                TargetDescription::from_slice(data, &[HashAlgorithm::Sha256]).unwrap();
585            let path = TargetPath::new("batty").unwrap();
586            client.store_target(&path, &mut &*data).await.unwrap();
587
588            let mut read = client
589                .fetch_target(false, &path, target_description)
590                .await
591                .unwrap();
592            let mut buf = Vec::new();
593            read.read_to_end(&mut buf).await.unwrap();
594            assert_eq!(buf.as_slice(), data);
595        })
596    }
597
598    #[test]
599    fn repository_dyn_impls_repository_traits() {
600        let mut repo = EphemeralRepository::new();
601
602        fn storage<T: RepositoryStorage<Pouf1>>(_t: T) {}
603        fn provider<T: RepositoryProvider<Pouf1>>(_t: T) {}
604
605        provider(&repo as &dyn RepositoryProvider<Pouf1>);
606        provider(&mut repo as &mut dyn RepositoryProvider<Pouf1>);
607        storage(&mut repo as &mut dyn RepositoryStorage<Pouf1>);
608    }
609}