Skip to content

Commit

Permalink
fix: timestamp.json meta can has optional fields
Browse files Browse the repository at this point in the history
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 awslabs#771

Signed-off-by: Flavio Castelli <[email protected]>
  • Loading branch information
flavio committed Aug 8, 2024
1 parent eb5e25e commit f2a8cce
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 51 deletions.
5 changes: 3 additions & 2 deletions tough/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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<u64> {
fn max_snapshot_size(&self) -> Result<Option<u64>> {
let snapshot_meta =
self.timestamp()
.signed
Expand Down
22 changes: 11 additions & 11 deletions tough/src/editor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -700,13 +700,13 @@ impl RepositoryEditor {
Ok(snapshot)
}

/// Build a `SnapshotMeta` struct from a given `SignedRole<R>`. This metadata
/// Build a `Metafiles` struct from a given `SignedRole<R>`. This metadata
/// includes the sha256 and length of the signed role.
fn snapshot_meta<R>(role: &SignedRole<R>) -> SnapshotMeta
fn snapshot_meta<R>(role: &SignedRole<R>) -> Metafile
where
R: Role,
{
SnapshotMeta {
Metafile {
hashes: Some(Hashes {
sha256: role.sha256.to_vec().into(),
_extra: HashMap::new(),
Expand Down Expand Up @@ -738,18 +738,18 @@ impl RepositoryEditor {
Ok(timestamp)
}

/// Build a `TimestampMeta` struct from a given `SignedRole<R>`. This metadata
/// Build a `Metafiles` struct from a given `SignedRole<R>`. This metadata
/// includes the sha256 and length of the signed role.
fn timestamp_meta<R>(role: &SignedRole<R>) -> TimestampMeta
fn timestamp_meta<R>(role: &SignedRole<R>) -> 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(),
}
Expand Down
39 changes: 29 additions & 10 deletions tough/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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,
}
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -360,6 +365,7 @@ impl Repository {
transport.as_ref(),
&root,
&timestamp,
limits.max_snapshot_size,
&datastore,
&metadata_base_url,
expiration_enforcement,
Expand Down Expand Up @@ -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<Root>,
timestamp: &Signed<Timestamp>,
max_snapshot_size: u64,
datastore: &Datastore,
metadata_base_url: &Url,
expiration_enforcement: ExpirationEnforcement,
Expand Down Expand Up @@ -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
Expand Down
33 changes: 5 additions & 28 deletions tough/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,11 @@ pub struct Snapshot {
/// Determines when metadata should be considered expired and no longer trusted by clients.
pub expires: DateTime<Utc>,

/// 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<String, SnapshotMeta>,
pub meta: HashMap<String, Metafile>,

/// Extra arguments found during deserialization.
///
Expand All @@ -272,7 +272,7 @@ pub struct Snapshot {
pub _extra: HashMap<String, Value>,
}

/// 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 : {
Expand All @@ -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.
Expand Down Expand Up @@ -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<String, TimestampMeta>,
pub meta: HashMap<String, Metafile>,

/// Extra arguments found during deserialization.
///
Expand All @@ -1124,29 +1124,6 @@ pub struct Timestamp {
pub _extra: HashMap<String, Value>,
}

/// 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<String, Value>,
}

impl Timestamp {
/// Creates a new `Timestamp` object.
pub fn new(spec_version: String, version: NonZeroU64, expires: DateTime<Utc>) -> Self {
Expand Down
1 change: 1 addition & 0 deletions tough/tests/interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down

0 comments on commit f2a8cce

Please sign in to comment.