Skip to content

Commit 598111f

Browse files
committed
tough: implement terminating delegations
In the pre-order depth-first search for target metadata, if a delegated role is selected, is terminating, and does not have metadata for the target, end the search. TUF specification v1.0.33 section 5.6.7.2.1.
1 parent c5ee171 commit 598111f

9 files changed

Lines changed: 85 additions & 19 deletions

File tree

tough/src/editor/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ impl RepositoryEditor {
352352
name: &str,
353353
key_source: &[Box<dyn KeySource>],
354354
paths: PathSet,
355+
terminating: bool,
355356
threshold: NonZeroU64,
356357
expiration: DateTime<Utc>,
357358
version: NonZeroU64,
@@ -387,6 +388,7 @@ impl RepositoryEditor {
387388
self.targets_editor_mut()?.delegate_role(
388389
new_targets,
389390
paths,
391+
terminating,
390392
key_pairs,
391393
keyids,
392394
threshold,
@@ -646,6 +648,7 @@ impl RepositoryEditor {
646648
name: &str,
647649
metadata_url: &str,
648650
paths: PathSet,
651+
terminating: bool,
649652
threshold: NonZeroU64,
650653
keys: Option<HashMap<Decoded<Hex>, Key>>,
651654
) -> Result<&mut Self> {
@@ -658,7 +661,7 @@ impl RepositoryEditor {
658661
self.targets_editor_mut()?.limits(limits);
659662
self.targets_editor_mut()?.transport(transport.clone());
660663
self.targets_editor_mut()?
661-
.add_role(name, metadata_url, paths, threshold, keys)
664+
.add_role(name, metadata_url, paths, terminating, threshold, keys)
662665
.await?;
663666

664667
Ok(self)

tough/src/editor/targets.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ impl TargetsEditor {
336336
&mut self,
337337
targets: Signed<DelegatedTargets>,
338338
paths: PathSet,
339+
terminating: bool,
339340
key_pairs: HashMap<Decoded<Hex>, Key>,
340341
keyids: Vec<Decoded<Hex>>,
341342
threshold: NonZeroU64,
@@ -348,7 +349,7 @@ impl TargetsEditor {
348349
paths,
349350
keyids,
350351
threshold,
351-
terminating: false,
352+
terminating,
352353
targets: Some(Signed {
353354
signed: targets.signed.targets,
354355
signatures: targets.signatures,
@@ -388,6 +389,7 @@ impl TargetsEditor {
388389
name: &str,
389390
metadata_url: &str,
390391
paths: PathSet,
392+
terminating: bool,
391393
threshold: NonZeroU64,
392394
keys: Option<HashMap<Decoded<Hex>, Key>>,
393395
) -> Result<&mut Self> {
@@ -446,7 +448,14 @@ impl TargetsEditor {
446448
(key_pairs.keys().cloned().collect(), key_pairs)
447449
};
448450

449-
self.delegate_role(delegated_targets, paths, key_pairs, keyids, threshold)?;
451+
self.delegate_role(
452+
delegated_targets,
453+
paths,
454+
terminating,
455+
key_pairs,
456+
keyids,
457+
threshold,
458+
)?;
450459

451460
Ok(self)
452461
}

tough/src/lib.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -483,12 +483,20 @@ impl Repository {
483483
// HASH is one of the hashes of the targets file listed in the targets metadata file
484484
// found earlier in step 4. In either case, the client MUST write the file to
485485
// non-volatile storage as FILENAME.EXT.
486-
Ok(if let Ok(target) = self.targets.signed.find_target(name) {
487-
let (sha256, file) = self.target_digest_and_filename(target, name);
488-
Some(self.fetch_target(target, &sha256, file.as_str()).await?)
489-
} else {
490-
None
491-
})
486+
let mut visited_roles: BTreeSet<String> = BTreeSet::new();
487+
let mut terminated = false;
488+
Ok(
489+
if let Ok(target) =
490+
self.targets
491+
.signed
492+
.find_target(name, &mut visited_roles, &mut terminated, false)
493+
{
494+
let (sha256, file) = self.target_digest_and_filename(target, name);
495+
Some(self.fetch_target(target, &sha256, file.as_str()).await?)
496+
} else {
497+
None
498+
},
499+
)
492500
}
493501

494502
/// Fetches a target from the repository and saves it to `outdir`. Attempts to do this as safely
@@ -535,11 +543,15 @@ impl Repository {
535543

536544
let filename = match prepend {
537545
Prefix::Digest => {
538-
let target = self.targets.signed.find_target(name).with_context(|_| {
539-
error::CacheTargetMissingSnafu {
546+
let mut visited_roles: BTreeSet<String> = BTreeSet::new();
547+
let mut terminated = false;
548+
let target = self
549+
.targets
550+
.signed
551+
.find_target(name, &mut visited_roles, &mut terminated, false)
552+
.with_context(|_| error::CacheTargetMissingSnafu {
540553
target_name: name.clone(),
541-
}
542-
})?;
554+
})?;
543555
let sha256 = target.hashes.sha256.clone().into_vec();
544556
format!("{}.{}", hex::encode(sha256), name.resolved())
545557
}
@@ -1220,6 +1232,7 @@ async fn load_targets(
12201232
}
12211233

12221234
// Follow the paths of delegations starting with the top level targets.json delegation
1235+
#[allow(clippy::too_many_arguments)]
12231236
#[async_recursion]
12241237
async fn load_delegations(
12251238
transport: &dyn Transport,

tough/src/schema/mod.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
2727
use serde_json::Value;
2828
use serde_plain::{derive_display_from_serialize, derive_fromstr_from_deserialize};
2929
use snafu::ResultExt;
30-
use std::collections::HashMap;
30+
use std::collections::{BTreeSet, HashMap};
3131
use std::num::NonZeroU64;
3232
use std::ops::{Deref, DerefMut};
3333
use std::path::Path;
@@ -516,21 +516,43 @@ impl Targets {
516516
///
517517
/// **Caution**: does not imply that delegations in this struct or any child are valid.
518518
///
519-
pub fn find_target(&self, target_name: &TargetName) -> Result<&Target> {
519+
pub fn find_target(
520+
&self,
521+
target_name: &TargetName,
522+
visited: &mut BTreeSet<String>,
523+
terminated: &mut bool,
524+
permissive: bool,
525+
) -> Result<&Target> {
520526
if let Some(target) = self.targets.get(target_name) {
521527
return Ok(target);
522528
}
523529
if let Some(delegations) = &self.delegations {
524530
for role in &delegations.roles {
525531
// If the target cannot match this DelegatedRole, then we do not want to recurse and
526-
// check any of its child roles either.
527-
if !role.paths.matches_target_name(target_name) {
532+
// check any of its child roles either. If we have already visited this role, we
533+
// do not need to visit it again.
534+
if !role.paths.matches_target_name(target_name) || visited.contains(&role.name) {
528535
continue;
529536
}
537+
visited.insert(role.name.clone());
530538
if let Some(targets) = &role.targets {
531-
if let Ok(target) = targets.signed.find_target(target_name) {
539+
if let Ok(target) =
540+
targets
541+
.signed
542+
.find_target(target_name, visited, terminated, permissive)
543+
{
532544
return Ok(target);
533545
}
546+
if !permissive && *terminated {
547+
// we encountered a terminating delegation, so we stop iterating immediately
548+
break;
549+
}
550+
}
551+
if role.terminating && !permissive {
552+
// this role was terminating, so set terminated. This will cause all ancestors
553+
// to stop iterating and return not-found.
554+
*terminated = true;
555+
break;
534556
}
535557
}
536558
}
@@ -724,7 +746,9 @@ impl Targets {
724746
/// that the ownership of each target is valid.
725747
pub(crate) fn validate(&self) -> Result<()> {
726748
for (target_name, _) in self.targets_iter() {
727-
self.find_target(target_name)?;
749+
let mut terminated = false;
750+
let mut visited_roles: BTreeSet<String> = BTreeSet::new();
751+
self.find_target(target_name, &mut visited_roles, &mut terminated, true)?;
728752
}
729753
Ok(())
730754
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is delegated a.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is delegation b.txt

tough/tests/repo_editor.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ async fn create_sign_write_reload_repo() {
169169
"role1",
170170
role1_key,
171171
PathSet::Paths(vec![PathPattern::new("file?.txt").unwrap()]),
172+
false,
172173
NonZeroU64::new(1).unwrap(),
173174
Utc::now().checked_add_signed(days(21)).unwrap(),
174175
NonZeroU64::new(1).unwrap(),
@@ -189,6 +190,7 @@ async fn create_sign_write_reload_repo() {
189190
"role2",
190191
role2_key,
191192
PathSet::Paths(vec![PathPattern::new("file1.txt").unwrap()]),
193+
false,
192194
NonZeroU64::new(1).unwrap(),
193195
Utc::now().checked_add_signed(days(21)).unwrap(),
194196
NonZeroU64::new(1).unwrap(),
@@ -199,6 +201,7 @@ async fn create_sign_write_reload_repo() {
199201
"role3",
200202
role1_key,
201203
PathSet::Paths(vec![PathPattern::new("file1.txt").unwrap()]),
204+
false,
202205
NonZeroU64::new(1).unwrap(),
203206
Utc::now().checked_add_signed(days(21)).unwrap(),
204207
NonZeroU64::new(1).unwrap(),
@@ -219,6 +222,7 @@ async fn create_sign_write_reload_repo() {
219222
"role4",
220223
role2_key,
221224
PathSet::Paths(vec![PathPattern::new("file1.txt").unwrap()]),
225+
false,
222226
NonZeroU64::new(1).unwrap(),
223227
Utc::now().checked_add_signed(days(21)).unwrap(),
224228
NonZeroU64::new(1).unwrap(),
@@ -312,6 +316,7 @@ async fn create_role_flow() {
312316
"A",
313317
metadata_base_url_out.as_str(),
314318
PathSet::Paths(vec![PathPattern::new("*.txt").unwrap()]),
319+
false,
315320
NonZeroU64::new(1).unwrap(),
316321
Some(key_hash_map(role1_key).await),
317322
)
@@ -396,6 +401,7 @@ async fn create_role_flow() {
396401
"B",
397402
metadata_base_url_out.as_str(),
398403
PathSet::Paths(vec![PathPattern::new("file?.txt").unwrap()]),
404+
false,
399405
NonZeroU64::new(1).unwrap(),
400406
Some(key_hash_map(role2_key).await),
401407
)
@@ -536,6 +542,7 @@ async fn update_targets_flow() {
536542
"A",
537543
metadata_base_url_out.as_str(),
538544
PathSet::Paths(vec![PathPattern::new("*.txt").unwrap()]),
545+
false,
539546
NonZeroU64::new(1).unwrap(),
540547
Some(key_hash_map(role1_key).await),
541548
)
@@ -620,6 +627,7 @@ async fn update_targets_flow() {
620627
"B",
621628
metadata_base_url_out.as_str(),
622629
PathSet::Paths(vec![PathPattern::new("file?.txt").unwrap()]),
630+
false,
623631
NonZeroU64::new(1).unwrap(),
624632
Some(key_hash_map(role2_key).await),
625633
)

tough/tests/target_path_safety.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ async fn safe_target_paths() {
9191
"delegated",
9292
&keys,
9393
PathSet::Paths(vec![PathPattern::new("delegated/*").unwrap()]),
94+
false,
9495
one,
9596
later(),
9697
one,

tuftool/src/add_role.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ pub(crate) struct AddRoleArgs {
4545
#[arg(short, long, conflicts_with = "path_hash_prefixes")]
4646
paths: Option<Vec<PathPattern>>,
4747

48+
/// Make the delegation terminating
49+
#[arg(long)]
50+
terminating: bool,
51+
4852
/// Path to root.json file for the repository
4953
#[arg(short, long)]
5054
root: PathBuf,
@@ -130,6 +134,7 @@ impl AddRoleArgs {
130134
&self.delegatee,
131135
self.indir.as_str(),
132136
paths,
137+
self.terminating,
133138
self.threshold,
134139
None,
135140
)
@@ -205,6 +210,7 @@ impl AddRoleArgs {
205210
&self.delegatee,
206211
self.indir.as_str(),
207212
paths,
213+
self.terminating,
208214
self.threshold,
209215
None,
210216
)

0 commit comments

Comments
 (0)