From 8052f32a526325f6a3c3f7e4b7c8a41db89dde1b Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Mon, 1 Jul 2024 16:50:19 +0200 Subject: [PATCH] fix: timestamp.json meta can has optional fields According to the TUF specification, the `meta` attribute of `timestamp.json` must follow the same specification of `METAFILES`. That means it has optional `LENGTH` and `HASHES`. See [this](https://theupdateframework.github.io/specification/latest/#file-formats-timestamp) section of the TUF specification. Fixes issue https://github.com/awslabs/tough/issues/771 Signed-off-by: Flavio Castelli --- tough/src/cache.rs | 5 +++-- tough/src/editor/mod.rs | 22 +++++++++++----------- tough/src/lib.rs | 39 +++++++++++++++++++++++++++++---------- tough/src/schema/mod.rs | 33 +++++---------------------------- tough/tests/interop.rs | 1 + 5 files changed, 49 insertions(+), 51 deletions(-) diff --git a/tough/src/cache.rs b/tough/src/cache.rs index 97e705020..39cc9f02e 100644 --- a/tough/src/cache.rs +++ b/tough/src/cache.rs @@ -96,7 +96,8 @@ impl Repository { { self.cache_file_from_transport( self.snapshot_filename().as_str(), - self.max_snapshot_size()?, + self.max_snapshot_size()? + .unwrap_or(self.limits.max_snapshot_size), "timestamp.json", &metadata_outdir, ) @@ -237,7 +238,7 @@ impl Repository { } /// Gets the max size of the snapshot.json file as specified by the timestamp file. - fn max_snapshot_size(&self) -> Result { + fn max_snapshot_size(&self) -> Result> { let snapshot_meta = self.timestamp() .signed diff --git a/tough/src/editor/mod.rs b/tough/src/editor/mod.rs index 377d725ef..6ec91b278 100644 --- a/tough/src/editor/mod.rs +++ b/tough/src/editor/mod.rs @@ -17,8 +17,8 @@ use crate::key_source::KeySource; use crate::schema::decoded::{Decoded, Hex}; use crate::schema::key::Key; use crate::schema::{ - Hashes, KeyHolder, PathSet, Role, RoleType, Root, Signed, Snapshot, SnapshotMeta, Target, - Targets, Timestamp, TimestampMeta, + Hashes, KeyHolder, Metafile, PathSet, Role, RoleType, Root, Signed, Snapshot, Target, Targets, + Timestamp, }; use crate::transport::{IntoVec, Transport}; use crate::{encode_filename, Limits}; @@ -700,13 +700,13 @@ impl RepositoryEditor { Ok(snapshot) } - /// Build a `SnapshotMeta` struct from a given `SignedRole`. This metadata + /// Build a `Metafiles` struct from a given `SignedRole`. This metadata /// includes the sha256 and length of the signed role. - fn snapshot_meta(role: &SignedRole) -> SnapshotMeta + fn snapshot_meta(role: &SignedRole) -> Metafile where R: Role, { - SnapshotMeta { + Metafile { hashes: Some(Hashes { sha256: role.sha256.to_vec().into(), _extra: HashMap::new(), @@ -738,18 +738,18 @@ impl RepositoryEditor { Ok(timestamp) } - /// Build a `TimestampMeta` struct from a given `SignedRole`. This metadata + /// Build a `Metafiles` struct from a given `SignedRole`. This metadata /// includes the sha256 and length of the signed role. - fn timestamp_meta(role: &SignedRole) -> TimestampMeta + fn timestamp_meta(role: &SignedRole) -> Metafile where R: Role, { - TimestampMeta { - hashes: Hashes { + Metafile { + hashes: Some(Hashes { sha256: role.sha256.to_vec().into(), _extra: HashMap::new(), - }, - length: role.length, + }), + length: Some(role.length), version: role.signed.signed.version(), _extra: HashMap::new(), } diff --git a/tough/src/lib.rs b/tough/src/lib.rs index 57acd5dae..0b5f6d699 100644 --- a/tough/src/lib.rs +++ b/tough/src/lib.rs @@ -250,8 +250,8 @@ impl<'a> RepositoryLoader<'a> { /// are set higher than what would reasonably be expected by a repository, but not so high that the /// amount of data could interfere with the system. /// -/// `max_root_size` and `max_timestamp_size` are the maximum size for the `root.json` and -/// `timestamp.json` files, respectively, downloaded from the repository. These must be +/// `max_root_size`, `max_timestamp_size` and `max_snapshot_size` are the maximum size for the `root.json`, +/// `timestamp.json` and `snapshot.json` files, respectively, downloaded from the repository. These must be /// sufficiently large such that future updates to your repository's key management strategy /// will still be supported, but sufficiently small such that you are protected against an /// endless data attack (defined by TUF as an attacker responding to clients with extremely @@ -261,6 +261,7 @@ impl<'a> RepositoryLoader<'a> { /// * `max_root_size`: 1 MiB /// * `max_targets_size`: 10 MiB /// * `max_timestamp_size`: 1 MiB +/// * `max_snapshot_size`: 1 MiB /// * `max_root_updates`: 1024 #[derive(Debug, Clone, Copy)] pub struct Limits { @@ -275,6 +276,9 @@ pub struct Limits { /// The maximum allowable size in bytes for the downloaded timestamp.json file. pub max_timestamp_size: u64, + /// The maximum allowable size in bytes for the downloaded snapshot.json file. + pub max_snapshot_size: u64, + /// The maximum number of updates to root.json to download. pub max_root_updates: u64, } @@ -285,6 +289,7 @@ impl Default for Limits { max_root_size: 1024 * 1024, // 1 MiB max_targets_size: 1024 * 1024 * 10, // 10 MiB max_timestamp_size: 1024 * 1024, // 1 MiB + max_snapshot_size: 1024 * 1024, // 1 MiB max_root_updates: 1024, } } @@ -360,6 +365,7 @@ impl Repository { transport.as_ref(), &root, ×tamp, + limits.max_snapshot_size, &datastore, &metadata_base_url, expiration_enforcement, @@ -906,10 +912,12 @@ async fn load_timestamp( } /// Step 3 of the client application, which loads the snapshot metadata file. +#[allow(clippy::too_many_lines)] async fn load_snapshot( transport: &dyn Transport, root: &Signed, timestamp: &Signed, + max_snapshot_size: u64, datastore: &Datastore, metadata_base_url: &Url, expiration_enforcement: ExpirationEnforcement, @@ -941,14 +949,25 @@ async fn load_snapshot( path: path.clone(), url: metadata_base_url.clone(), })?; - let stream = fetch_sha256( - transport, - url.clone(), - snapshot_meta.length, - "timestamp.json", - &snapshot_meta.hashes.sha256, - ) - .await?; + let stream = if let Some(hashes) = &snapshot_meta.hashes { + fetch_sha256( + transport, + url.clone(), + snapshot_meta.length.unwrap_or(max_snapshot_size), + "timestamp.json", + &hashes.sha256, + ) + .await? + } else { + fetch_max_size( + transport, + url.clone(), + snapshot_meta.length.unwrap_or(max_snapshot_size), + "timestamp.json", + ) + .await? + }; + let data = stream .into_vec() .await diff --git a/tough/src/schema/mod.rs b/tough/src/schema/mod.rs index 264440e71..72efa3328 100644 --- a/tough/src/schema/mod.rs +++ b/tough/src/schema/mod.rs @@ -256,11 +256,11 @@ pub struct Snapshot { /// Determines when metadata should be considered expired and no longer trusted by clients. pub expires: DateTime, - /// A list of what the TUF spec calls 'METAFILES' (`SnapshotMeta` objects). The TUF spec + /// A list of what the TUF spec calls 'METAFILES' (`Metafiles` objects). The TUF spec /// describes the hash key in 4.4: METAPATH is the file path of the metadata on the repository /// relative to the metadata base URL. For snapshot.json, these are top-level targets metadata /// and delegated targets metadata. - pub meta: HashMap, + pub meta: HashMap, /// Extra arguments found during deserialization. /// @@ -272,7 +272,7 @@ pub struct Snapshot { pub _extra: HashMap, } -/// Represents a metadata file in a `snapshot.json` file. +/// Represents a metadata file in a `snapshot.json` and in a `timestamp.json` file. /// TUF 4.4: METAFILES is an object whose format is the following: /// ```text /// { METAPATH : { @@ -292,7 +292,7 @@ pub struct Snapshot { /// }, /// ``` #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] -pub struct SnapshotMeta { +pub struct Metafile { /// LENGTH is the integer length in bytes of the metadata file at METAPATH. It is OPTIONAL and /// can be omitted to reduce the snapshot metadata file size. In that case the client MUST use a /// custom download limit for the listed metadata. @@ -1112,7 +1112,7 @@ pub struct Timestamp { /// METAFILES is the same as described for the snapshot.json file. In the case of the /// timestamp.json file, this MUST only include a description of the snapshot.json file. - pub meta: HashMap, + pub meta: HashMap, /// Extra arguments found during deserialization. /// @@ -1124,29 +1124,6 @@ pub struct Timestamp { pub _extra: HashMap, } -/// METAFILES is the same as described for the snapshot.json file. In the case of the timestamp.json -/// file, this MUST only include a description of the snapshot.json file. -#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] -pub struct TimestampMeta { - /// The integer length in bytes of the snapshot.json file. - pub length: u64, - - /// The hashes of the snapshot.json file. - pub hashes: Hashes, - - /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version - /// number less than the one currently trusted. - pub version: NonZeroU64, - - /// Extra arguments found during deserialization. - /// - /// We must store these to correctly verify signatures for this object. - /// - /// If you're instantiating this struct, you should make this `HashMap::empty()`. - #[serde(flatten)] - pub _extra: HashMap, -} - impl Timestamp { /// Creates a new `Timestamp` object. pub fn new(spec_version: String, version: NonZeroU64, expires: DateTime) -> Self { diff --git a/tough/tests/interop.rs b/tough/tests/interop.rs index a60b5a359..2e6781e78 100644 --- a/tough/tests/interop.rs +++ b/tough/tests/interop.rs @@ -80,6 +80,7 @@ async fn test_tuf_reference_impl_default_transport() { max_root_size: 1000, max_targets_size: 2000, max_timestamp_size: 3000, + max_snapshot_size: 4000, max_root_updates: 1, }) .datastore(datastore.path())