Skip to content

Commit 0eeb60a

Browse files
committed
tough: enforce root metadata validation
Update metadata validation: must not skip versions, must measure expiration relative to start of update. TUF specification v1.0.33, sections 5.1 and 5.3.5.
1 parent 7573eb9 commit 0eeb60a

File tree

1 file changed

+41
-37
lines changed

1 file changed

+41
-37
lines changed

tough/src/lib.rs

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ impl Repository {
336336
let expiration_enforcement = loader.expiration_enforcement.unwrap_or_default();
337337
let metadata_base_url = parse_url(loader.metadata_base_url)?;
338338
let targets_base_url = parse_url(loader.targets_base_url)?;
339+
let update_start = datastore.system_time().await?;
339340

340341
// 0. Load the trusted root metadata file + 1. Update the root metadata file
341342
let root = load_root(
@@ -346,6 +347,7 @@ impl Repository {
346347
limits.max_root_updates,
347348
&metadata_base_url,
348349
expiration_enforcement,
350+
&update_start,
349351
)
350352
.await?;
351353

@@ -357,6 +359,7 @@ impl Repository {
357359
limits.max_timestamp_size,
358360
&metadata_base_url,
359361
expiration_enforcement,
362+
&update_start,
360363
)
361364
.await?;
362365

@@ -369,6 +372,7 @@ impl Repository {
369372
&datastore,
370373
&metadata_base_url,
371374
expiration_enforcement,
375+
&update_start,
372376
)
373377
.await?;
374378

@@ -381,6 +385,7 @@ impl Repository {
381385
limits.max_targets_size,
382386
&metadata_base_url,
383387
expiration_enforcement,
388+
&update_start,
384389
)
385390
.await?;
386391

@@ -633,9 +638,9 @@ pub(crate) fn encode_filename<S: AsRef<str>>(name: S) -> String {
633638

634639
/// TUF v1.0.16, 5.2.9, 5.3.3, 5.4.5, 5.5.4, The expiration timestamp in the `[metadata]` file MUST
635640
/// be higher than the fixed update start time.
636-
async fn check_expired<T: Role>(datastore: &Datastore, role: &T) -> Result<()> {
641+
fn check_expired<T: Role>(update_start: &DateTime<Utc>, role: &T) -> Result<()> {
637642
ensure!(
638-
datastore.system_time().await? <= role.expires(),
643+
*update_start <= role.expires(),
639644
error::ExpiredMetadataSnafu { role: T::TYPE }
640645
);
641646
Ok(())
@@ -656,6 +661,7 @@ fn parse_url(url: Url) -> Result<Url> {
656661

657662
/// Steps 0 and 1 of the client application, which load the current root metadata file based on a
658663
/// trusted root metadata file.
664+
#[allow(clippy::too_many_arguments)]
659665
async fn load_root<R: AsRef<[u8]>>(
660666
transport: &dyn Transport,
661667
root: R,
@@ -664,8 +670,9 @@ async fn load_root<R: AsRef<[u8]>>(
664670
max_root_updates: u64,
665671
metadata_base_url: &Url,
666672
expiration_enforcement: ExpirationEnforcement,
673+
update_start: &DateTime<Utc>,
667674
) -> Result<Signed<Root>> {
668-
// 0. Load the trusted root metadata file. We assume that a good, trusted copy of this file was
675+
// 5.2. Load the trusted root metadata file. We assume that a good, trusted copy of this file was
669676
// shipped with the package manager or software updater using an out-of-band process. Note
670677
// that the expiration of the trusted root metadata file does not matter, because we will
671678
// attempt to update it in the next step.
@@ -675,7 +682,7 @@ async fn load_root<R: AsRef<[u8]>>(
675682
.verify_role(&root)
676683
.context(error::VerifyTrustedMetadataSnafu)?;
677684

678-
// Used in step 1.2
685+
// Used in step 5.3
679686
let original_root_version = root.signed.version.get();
680687

681688
// Used in step 1.9
@@ -690,15 +697,15 @@ async fn load_root<R: AsRef<[u8]>>(
690697
.cloned()
691698
.collect::<Vec<_>>();
692699

693-
// 1. Update the root metadata file. Since it may now be signed using entirely different keys,
700+
// 5.3. Update the root metadata file. Since it may now be signed using entirely different keys,
694701
// the client must somehow be able to establish a trusted line of continuity to the latest
695702
// set of keys. To do so, the client MUST download intermediate root metadata files, until
696703
// the latest available one is reached. Therefore, it MUST temporarily turn on consistent
697704
// snapshots in order to download versioned root metadata files as described next.
698705
loop {
699-
// 1.1. Let N denote the version number of the trusted root metadata file.
706+
// 5.3.2. Let N denote the version number of the trusted root metadata file.
700707
//
701-
// 1.2. Try downloading version N+1 of the root metadata file, up to some X number of bytes
708+
// 5.3.3. Try downloading version N+1 of the root metadata file, up to some X number of bytes
702709
// (because the size is unknown). The value for X is set by the authors of the
703710
// application using TUF. For example, X may be tens of kilobytes. The filename used to
704711
// download the root metadata file is of the fixed form VERSION_NUMBER.FILENAME.EXT
@@ -725,7 +732,7 @@ async fn load_root<R: AsRef<[u8]>>(
725732
)
726733
.await
727734
{
728-
Err(_) => break, // If this file is not available, then go to step 1.8.
735+
Err(_) => break, // If this file is not available, then go to step 5.3.10.
729736
Ok(stream) => {
730737
let data = match stream.into_vec().await {
731738
Ok(d) => d,
@@ -737,7 +744,7 @@ async fn load_root<R: AsRef<[u8]>>(
737744
role: RoleType::Root,
738745
})?;
739746

740-
// 1.3. Check signatures. Version N+1 of the root metadata file MUST have been
747+
// 5.3.4. Check signatures. Version N+1 of the root metadata file MUST have been
741748
// signed by: (1) a threshold of keys specified in the trusted root metadata file
742749
// (version N), and (2) a threshold of keys specified in the new root metadata
743750
// file being validated (version N+1). If version N+1 is not signed as required,
@@ -755,56 +762,49 @@ async fn load_root<R: AsRef<[u8]>>(
755762
role: RoleType::Root,
756763
})?;
757764

758-
// 1.4. Check for a rollback attack. The version number of the trusted root
759-
// metadata file (version N) must be less than or equal to the version number of
760-
// the new root metadata file (version N+1). Effectively, this means checking
761-
// that the version number signed in the new root metadata file is indeed N+1. If
762-
// the version of the new root metadata file is less than the trusted metadata
763-
// file, discard it, abort the update cycle, and report the rollback attack. On
764-
// the next update cycle, begin at step 0 and version N of the root metadata
765-
// file.
765+
// 5.3.5. Check for a rollback attack. The version number of the new root
766+
// metadata (version N+1) MUST be exactly the version in the trusted root
767+
// metadata (version N) incremented by one, that is precisely N+1.
768+
// off-spec: protect the comparison against u64 overflow (if N < new value,
769+
// N+1 will not overflow).
766770
ensure!(
767-
root.signed.version <= new_root.signed.version,
771+
root.signed.version < new_root.signed.version
772+
&& root.signed.version.get() + 1 == new_root.signed.version.get(),
768773
error::OlderMetadataSnafu {
769774
role: RoleType::Root,
770775
current_version: root.signed.version,
771776
new_version: new_root.signed.version
772777
}
773778
);
774779

775-
// Off-spec: 1.4 specifies that the version number of the trusted root metadata
776-
// file must be less than or equal to the version number of the new root metadata
777-
// file. If they are equal, this will create an infinite loop, so we ignore the new
778-
// root metadata file but do not report an error. This could only happen if the
779-
// path we built above, referencing N+1, has a filename that doesn't match its
780-
// contents, which would have to list version N.
781-
if root.signed.version == new_root.signed.version {
782-
break;
783-
}
784-
785-
// 1.5. Note that the expiration of the new (intermediate) root metadata file does
780+
// 5.3.6. Note that the expiration of the new (intermediate) root metadata file does
786781
// not matter yet, because we will check for it in step 1.8.
787782
//
788-
// 1.6. Set the trusted root metadata file to the new root metadata file.
783+
// 5.3.7. Set the trusted root metadata file to the new root metadata file.
789784
//
790785
// (This is where version N+1 becomes version N.)
791786
root = new_root;
792787

793-
// 1.7. Repeat steps 1.1 to 1.7.
788+
// 5.3.8. Persist root metadata. The client MUST write the file to non-volatile storage
789+
// as FILENAME.EXT (e.g. root.json).
790+
datastore.remove("root.json").await?;
791+
datastore.create("root.json", &root).await?;
792+
793+
// 5.3.9. Repeat 5.3.2 through 5.3.9.
794794
continue;
795795
}
796796
}
797797
}
798798

799-
datastore.remove("root.json");
799+
datastore.remove("root.json").await?;
800800
datastore.create("root.json", &root).await?;
801801

802802
// TUF v1.0.16, 5.2.9. Check for a freeze attack. The expiration timestamp in the trusted root
803803
// metadata file MUST be higher than the fixed update start time. If the trusted root metadata
804804
// file has expired, abort the update cycle, report the potential freeze attack. On the next
805805
// update cycle, begin at step 5.1 and version N of the root metadata file.
806806
if expiration_enforcement == ExpirationEnforcement::Safe {
807-
check_expired(datastore, &root.signed).await?;
807+
check_expired(update_start, &root.signed)?;
808808
}
809809

810810
// 1.9. If the timestamp and / or snapshot keys have been rotated, then delete the trusted
@@ -842,6 +842,7 @@ async fn load_timestamp(
842842
max_timestamp_size: u64,
843843
metadata_base_url: &Url,
844844
expiration_enforcement: ExpirationEnforcement,
845+
update_start: &DateTime<Utc>,
845846
) -> Result<Signed<Timestamp>> {
846847
// 2. Download the timestamp metadata file, up to Y number of bytes (because the size is
847848
// unknown.) The value for Y is set by the authors of the application using TUF. For
@@ -905,7 +906,7 @@ async fn load_timestamp(
905906
// metadata file becomes the trusted timestamp metadata file. If the new timestamp metadata file
906907
// has expired, discard it, abort the update cycle, and report the potential freeze attack.
907908
if expiration_enforcement == ExpirationEnforcement::Safe {
908-
check_expired(datastore, &timestamp.signed).await?;
909+
check_expired(update_start, &timestamp.signed)?;
909910
}
910911

911912
// Now that everything seems okay, write the timestamp file to the datastore.
@@ -915,7 +916,7 @@ async fn load_timestamp(
915916
}
916917

917918
/// Step 3 of the client application, which loads the snapshot metadata file.
918-
#[allow(clippy::too_many_lines)]
919+
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
919920
async fn load_snapshot(
920921
transport: &dyn Transport,
921922
root: &Signed<Root>,
@@ -924,6 +925,7 @@ async fn load_snapshot(
924925
datastore: &Datastore,
925926
metadata_base_url: &Url,
926927
expiration_enforcement: ExpirationEnforcement,
928+
update_start: &DateTime<Utc>,
927929
) -> Result<Signed<Snapshot>> {
928930
// 3. Download snapshot metadata file, up to the number of bytes specified in the timestamp
929931
// metadata file. If consistent snapshots are not used (see Section 7), then the filename
@@ -1062,7 +1064,7 @@ async fn load_snapshot(
10621064
// metadata file becomes the trusted snapshot metadata file. If the new snapshot metadata file
10631065
// is expired, discard it, abort the update cycle, and report the potential freeze attack.
10641066
if expiration_enforcement == ExpirationEnforcement::Safe {
1065-
check_expired(datastore, &snapshot.signed).await?;
1067+
check_expired(update_start, &snapshot.signed)?;
10661068
}
10671069

10681070
// Now that everything seems okay, write the snapshot file to the datastore.
@@ -1072,6 +1074,7 @@ async fn load_snapshot(
10721074
}
10731075

10741076
/// Step 4 of the client application, which loads the targets metadata file.
1077+
#[allow(clippy::too_many_arguments)]
10751078
async fn load_targets(
10761079
transport: &dyn Transport,
10771080
root: &Signed<Root>,
@@ -1080,6 +1083,7 @@ async fn load_targets(
10801083
max_targets_size: u64,
10811084
metadata_base_url: &Url,
10821085
expiration_enforcement: ExpirationEnforcement,
1086+
update_start: &DateTime<Utc>,
10831087
) -> Result<Signed<crate::schema::Targets>> {
10841088
// 4. Download the top-level targets metadata file, up to either the number of bytes specified
10851089
// in the snapshot metadata file, or some Z number of bytes. The value for Z is set by the
@@ -1186,7 +1190,7 @@ async fn load_targets(
11861190
// metadata file becomes the trusted targets metadata file. If the new targets metadata file is
11871191
// expired, discard it, abort the update cycle, and report the potential freeze attack.
11881192
if expiration_enforcement == ExpirationEnforcement::Safe {
1189-
check_expired(datastore, &targets.signed).await?;
1193+
check_expired(update_start, &targets.signed)?;
11901194
}
11911195

11921196
// Now that everything seems okay, write the targets file to the datastore.

0 commit comments

Comments
 (0)