tough/schema/mod.rs
1#![allow(clippy::used_underscore_binding, clippy::pub_underscore_fields)] // #20
2
3//! Provides the schema objects as defined by the TUF spec.
4
5mod de;
6pub mod decoded;
7mod error;
8mod iter;
9pub mod key;
10mod ser;
11mod spki;
12mod verify;
13
14use crate::schema::decoded::{Decoded, Hex};
15pub use crate::schema::error::{Error, Result};
16use crate::schema::iter::KeysIter;
17use crate::schema::key::Key;
18use crate::sign::Sign;
19pub use crate::transport::{FilesystemTransport, Transport};
20use crate::{encode_filename, TargetName};
21use aws_lc_rs::digest::{digest, Context, SHA256};
22use globset::{Glob, GlobMatcher};
23use hex::ToHex;
24use olpc_cjson::CanonicalFormatter;
25use serde::de::Error as SerdeDeError;
26use serde::{Deserialize, Deserializer, Serialize, Serializer};
27use serde_json::Value;
28use serde_plain::{derive_display_from_serialize, derive_fromstr_from_deserialize};
29use snafu::ResultExt;
30use std::collections::{BTreeSet, HashMap};
31use std::num::NonZeroU64;
32use std::ops::{Deref, DerefMut};
33use std::path::Path;
34use std::str::FromStr;
35use tokio::fs::File;
36use tokio::io::AsyncReadExt;
37
38/// The type of metadata role.
39#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)]
40#[serde(rename_all = "kebab-case")]
41pub enum RoleType {
42 /// The root role delegates trust to specific keys trusted for all other top-level roles used in
43 /// the system.
44 Root,
45 /// The snapshot role signs a metadata file that provides information about the latest version
46 /// of all targets metadata on the repository (the top-level targets role and all delegated
47 /// roles).
48 Snapshot,
49 /// The targets role's signature indicates which target files are trusted by clients.
50 Targets,
51 /// The timestamp role is used to prevent an adversary from replaying an out-of-date signed
52 /// metadata file whose signature has not yet expired.
53 Timestamp,
54 /// A delegated targets role
55 DelegatedTargets,
56}
57
58derive_display_from_serialize!(RoleType);
59derive_fromstr_from_deserialize!(RoleType);
60
61/// A role identifier
62#[derive(Debug, Clone)]
63pub enum RoleId {
64 /// Top level roles are identified by a `RoleType`
65 StandardRole(RoleType),
66 /// A delegated role is identified by a String
67 DelegatedRole(String),
68}
69
70/// Common trait implemented by all roles.
71pub trait Role: Serialize {
72 /// The type of role this object represents.
73 const TYPE: RoleType;
74
75 /// Determines when metadata should be considered expired and no longer trusted by clients.
76 fn expires(&self) -> jiff::Timestamp;
77
78 /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version
79 /// number less than the one currently trusted.
80 fn version(&self) -> NonZeroU64;
81
82 /// The filename that the role metadata should be written to
83 fn filename(&self, consistent_snapshot: bool) -> String;
84
85 /// The `RoleId` corresponding to the role
86 fn role_id(&self) -> RoleId {
87 RoleId::StandardRole(Self::TYPE)
88 }
89
90 /// A deterministic JSON serialization used when calculating the digest of a metadata object.
91 /// [More info on canonical JSON](http://wiki.laptop.org/go/Canonical_JSON)
92 fn canonical_form(&self) -> Result<Vec<u8>> {
93 let mut data = Vec::new();
94 let mut ser = serde_json::Serializer::with_formatter(&mut data, CanonicalFormatter::new());
95 self.serialize(&mut ser)
96 .context(error::JsonSerializationSnafu { what: "role" })?;
97 Ok(data)
98 }
99}
100
101/// A signed metadata object.
102#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
103pub struct Signed<T> {
104 /// The role that is signed.
105 pub signed: T,
106 /// A list of signatures and their key IDs.
107 pub signatures: Vec<Signature>,
108}
109
110/// A signature and the key ID that made it.
111#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
112pub struct Signature {
113 /// The key ID (listed in root.json) that made this signature.
114 pub keyid: Decoded<Hex>,
115 /// A hex-encoded signature of the canonical JSON form of a role.
116 pub sig: Decoded<Hex>,
117}
118
119/// A `KeyHolder` is metadata that is responsible for verifying the signatures of a role.
120/// `KeyHolder` contains either a `Delegations` of a `Targets` or a `Root`
121#[derive(Debug, Clone)]
122pub enum KeyHolder {
123 /// Delegations verify delegated targets
124 Delegations(Delegations),
125 /// Root verifies the top level targets, snapshot, timestamp, and root
126 Root(Root),
127}
128
129// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
130
131/// TUF 4.3: The root.json file is signed by the root role's keys. It indicates which keys are
132/// authorized for all top-level roles, including the root role itself. Revocation and replacement
133/// of top-level role keys, including for the root role, is done by changing the keys listed for the
134/// roles in this file.
135#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
136#[serde(tag = "_type")]
137#[serde(rename = "root")]
138pub struct Root {
139 /// A string that contains the version number of the TUF specification. Its format follows the
140 /// Semantic Versioning 2.0.0 (semver) specification.
141 pub spec_version: String,
142
143 /// A boolean indicating whether the repository supports consistent snapshots. When consistent
144 /// snapshots is `true`, targets and certain metadata filenames are prefixed with either a
145 /// a version number or digest.
146 pub consistent_snapshot: bool,
147
148 /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version
149 /// number less than the one currently trusted.
150 pub version: NonZeroU64,
151
152 /// Determines when metadata should be considered expired and no longer trusted by clients.
153 #[serde(serialize_with = "ser::serialize_timestamp")]
154 pub expires: jiff::Timestamp,
155
156 /// The KEYID must be correct for the specified KEY. Clients MUST calculate each KEYID to verify
157 /// this is correct for the associated key. Clients MUST ensure that for any KEYID represented
158 /// in this key list and in other files, only one unique key has that KEYID.
159 #[serde(deserialize_with = "de::deserialize_keys")]
160 pub keys: HashMap<Decoded<Hex>, Key>,
161
162 /// A list of roles, the keys associated with each role, and the threshold of signatures used
163 /// for each role.
164 pub roles: HashMap<RoleType, RoleKeys>,
165
166 /// Extra arguments found during deserialization.
167 ///
168 /// We must store these to correctly verify signatures for this object.
169 ///
170 /// If you're instantiating this struct, you should make this `HashMap::empty()`.
171 #[serde(flatten)]
172 #[serde(deserialize_with = "de::extra_skip_type")]
173 pub _extra: HashMap<String, Value>,
174}
175
176/// Represents the key IDs used for a role and the threshold of signatures required to validate it.
177/// TUF 4.3: A ROLE is one of "root", "snapshot", "targets", "timestamp", or "mirrors". A role for
178/// each of "root", "snapshot", "timestamp", and "targets" MUST be specified in the key list.
179/// The role of "mirror" is optional. If not specified, the mirror list will not need to be signed
180/// if mirror lists are being used. The THRESHOLD for a role is an integer of the number of keys of
181/// that role whose signatures are required in order to consider a file as being properly signed by
182/// that role.
183#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
184pub struct RoleKeys {
185 /// The key IDs used for the role.
186 pub keyids: Vec<Decoded<Hex>>,
187
188 /// The threshold of signatures required to validate the role.
189 pub threshold: NonZeroU64,
190
191 /// Extra arguments found during deserialization.
192 ///
193 /// We must store these to correctly verify signatures for this object.
194 ///
195 /// If you're instantiating this struct, you should make this `HashMap::empty()`.
196 #[serde(flatten)]
197 pub _extra: HashMap<String, Value>,
198}
199
200impl Root {
201 /// An iterator over the keys for a given role.
202 pub fn keys(&self, role: RoleType) -> impl Iterator<Item = &Key> {
203 KeysIter {
204 keyids_iter: match self.roles.get(&role) {
205 Some(role_keys) => role_keys.keyids.iter(),
206 None => [].iter(),
207 },
208 keys: &self.keys,
209 }
210 }
211
212 /// Given an object/key that impls Sign, return the corresponding
213 /// key ID from Root
214 pub fn key_id(&self, key_pair: &dyn Sign) -> Option<Decoded<Hex>> {
215 for (key_id, key) in &self.keys {
216 if key_pair.tuf_key() == *key {
217 return Some(key_id.clone());
218 }
219 }
220 None
221 }
222}
223
224impl Role for Root {
225 const TYPE: RoleType = RoleType::Root;
226
227 fn expires(&self) -> jiff::Timestamp {
228 self.expires
229 }
230
231 fn version(&self) -> NonZeroU64 {
232 self.version
233 }
234
235 fn filename(&self, _consistent_snapshot: bool) -> String {
236 format!("{}.root.json", self.version())
237 }
238}
239
240// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
241
242/// TUF 4.4 The snapshot.json file is signed by the snapshot role. It MUST list the version numbers
243/// of the top-level targets metadata and all delegated targets metadata. It MAY also list their
244/// lengths and file hashes.
245#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
246#[serde(tag = "_type")]
247#[serde(rename = "snapshot")]
248pub struct Snapshot {
249 /// A string that contains the version number of the TUF specification. Its format follows the
250 /// Semantic Versioning 2.0.0 (semver) specification.
251 pub spec_version: String,
252
253 /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version
254 /// number less than the one currently trusted.
255 pub version: NonZeroU64,
256
257 /// Determines when metadata should be considered expired and no longer trusted by clients.
258 #[serde(serialize_with = "ser::serialize_timestamp")]
259 pub expires: jiff::Timestamp,
260
261 /// A list of what the TUF spec calls 'METAFILES' (`Metafiles` objects). The TUF spec
262 /// describes the hash key in 4.4: METAPATH is the file path of the metadata on the repository
263 /// relative to the metadata base URL. For snapshot.json, these are top-level targets metadata
264 /// and delegated targets metadata.
265 pub meta: HashMap<String, Metafile>,
266
267 /// Extra arguments found during deserialization.
268 ///
269 /// We must store these to correctly verify signatures for this object.
270 ///
271 /// If you're instantiating this struct, you should make this `HashMap::empty()`.
272 #[serde(flatten)]
273 #[serde(deserialize_with = "de::extra_skip_type")]
274 pub _extra: HashMap<String, Value>,
275}
276
277/// Represents a metadata file in a `snapshot.json` and in a `timestamp.json` file.
278/// TUF 4.4: METAFILES is an object whose format is the following:
279/// ```text
280/// { METAPATH : {
281/// "version" : VERSION,
282/// ("length" : LENGTH, |
283/// "hashes" : HASHES) }
284/// , ...
285/// }
286/// ```
287/// e.g.
288/// ```json
289/// "project1.json": {
290/// "version": 1,
291/// "hashes": {
292/// "sha256": "f592d072e1193688a686267e8e10d7257b4ebfcf28133350dae88362d82a0c8a"
293/// }
294/// },
295/// ```
296#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
297pub struct Metafile {
298 /// LENGTH is the integer length in bytes of the metadata file at METAPATH. It is OPTIONAL and
299 /// can be omitted to reduce the snapshot metadata file size. In that case the client MUST use a
300 /// custom download limit for the listed metadata.
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub length: Option<u64>,
303
304 /// HASHES is a dictionary that specifies one or more hashes of the metadata file at METAPATH,
305 /// including their cryptographic hash function. For example: `{ "sha256": HASH, ... }`. HASHES
306 /// is OPTIONAL and can be omitted to reduce the snapshot metadata file size. In that case the
307 /// repository MUST guarantee that VERSION alone unambiguously identifies the metadata at
308 /// METAPATH.
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub hashes: Option<Hashes>,
311
312 /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version
313 /// number less than the one currently trusted.
314 pub version: NonZeroU64,
315
316 /// Extra arguments found during deserialization.
317 ///
318 /// We must store these to correctly verify signatures for this object.
319 ///
320 /// If you're instantiating this struct, you should make this `HashMap::empty()`.
321 #[serde(flatten)]
322 pub _extra: HashMap<String, Value>,
323}
324
325/// Represents the hash dictionary in a `snapshot.json` file.
326#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
327pub struct Hashes {
328 /// The SHA 256 digest of a metadata file.
329 pub sha256: Decoded<Hex>,
330
331 /// Extra arguments found during deserialization.
332 ///
333 /// We must store these to correctly verify signatures for this object.
334 ///
335 /// If you're instantiating this struct, you should make this `HashMap::empty()`.
336 #[serde(flatten)]
337 pub _extra: HashMap<String, Value>,
338}
339
340impl Snapshot {
341 /// Create a new `Snapshot` object.
342 pub fn new(spec_version: String, version: NonZeroU64, expires: jiff::Timestamp) -> Self {
343 Snapshot {
344 spec_version,
345 version,
346 expires,
347 meta: HashMap::new(),
348 _extra: HashMap::new(),
349 }
350 }
351}
352impl Role for Snapshot {
353 const TYPE: RoleType = RoleType::Snapshot;
354
355 fn expires(&self) -> jiff::Timestamp {
356 self.expires
357 }
358
359 fn version(&self) -> NonZeroU64 {
360 self.version
361 }
362
363 fn filename(&self, consistent_snapshot: bool) -> String {
364 if consistent_snapshot {
365 format!("{}.snapshot.json", self.version())
366 } else {
367 "snapshot.json".to_string()
368 }
369 }
370}
371
372// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
373
374/// Represents a `targets.json` file.
375/// TUF 4.5:
376/// The "signed" portion of targets.json is as follows:
377/// ```text
378/// { "_type" : "targets",
379/// "spec_version" : SPEC_VERSION,
380/// "version" : VERSION,
381/// "expires" : EXPIRES,
382/// "targets" : TARGETS,
383/// ("delegations" : DELEGATIONS)
384/// }
385/// ```
386///
387#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
388#[serde(tag = "_type")]
389#[serde(rename = "targets")]
390pub struct Targets {
391 /// A string that contains the version number of the TUF specification. Its format follows the
392 /// Semantic Versioning 2.0.0 (semver) specification.
393 pub spec_version: String,
394
395 /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version
396 /// number less than the one currently trusted.
397 pub version: NonZeroU64,
398
399 /// Determines when metadata should be considered expired and no longer trusted by clients.
400 #[serde(serialize_with = "ser::serialize_timestamp")]
401 pub expires: jiff::Timestamp,
402
403 /// Each key of the TARGETS object is a TARGETPATH. A TARGETPATH is a path to a file that is
404 /// relative to a mirror's base URL of targets.
405 pub targets: HashMap<TargetName, Target>,
406
407 /// Delegations describes subsets of the targets for which responsibility is delegated to
408 /// another role.
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub delegations: Option<Delegations>,
411
412 /// Extra arguments found during deserialization.
413 ///
414 /// We must store these to correctly verify signatures for this object.
415 ///
416 /// If you're instantiating this struct, you should make this `HashMap::empty()`.
417 #[serde(flatten)]
418 #[serde(deserialize_with = "de::extra_skip_type")]
419 pub _extra: HashMap<String, Value>,
420}
421
422/// TUF 4.5: TARGETS is an object whose format is the following:
423/// ```text
424/// { TARGETPATH : {
425/// "length" : LENGTH,
426/// "hashes" : HASHES,
427/// ("custom" : { ... }) }
428/// , ...
429/// }
430/// ```
431#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
432pub struct Target {
433 /// LENGTH is the integer length in bytes of the target file at TARGETPATH.
434 pub length: u64,
435
436 /// HASHES is a dictionary that specifies one or more hashes, including the cryptographic hash
437 /// function. For example: `{ "sha256": HASH, ... }`. HASH is the hexdigest of the cryptographic
438 /// function computed on the target file.
439 pub hashes: Hashes,
440
441 /// If defined, the elements and values of "custom" will be made available to the client
442 /// application. The information in "custom" is opaque to the framework and can include version
443 /// numbers, dependencies, requirements, and any other data that the application wants to
444 /// include to describe the file at TARGETPATH. The application may use this information to
445 /// guide download decisions.
446 #[serde(default)]
447 #[serde(skip_serializing_if = "HashMap::is_empty")]
448 pub custom: HashMap<String, Value>,
449
450 /// Extra arguments found during deserialization.
451 ///
452 /// We must store these to correctly verify signatures for this object.
453 ///
454 /// If you're instantiating this struct, you should make this `HashMap::empty()`.
455 #[serde(flatten)]
456 pub _extra: HashMap<String, Value>,
457}
458
459impl Target {
460 /// Given a path, returns a Target struct
461 pub async fn from_path<P>(path: P) -> Result<Target>
462 where
463 P: AsRef<Path>,
464 {
465 // Ensure the given path is a file
466 let path = path.as_ref();
467 if !path.is_file() {
468 return error::TargetNotAFileSnafu { path }.fail();
469 }
470
471 // Get the sha256 and length of the target
472 let mut file = File::open(path)
473 .await
474 .context(error::FileOpenSnafu { path })?;
475 let mut digest = Context::new(&SHA256);
476 let mut buf = [0; 8 * 1024];
477 let mut length = 0;
478 loop {
479 match file
480 .read(&mut buf)
481 .await
482 .context(error::FileReadSnafu { path })?
483 {
484 0 => break,
485 n => {
486 digest.update(&buf[..n]);
487 length += n as u64;
488 }
489 }
490 }
491
492 Ok(Target {
493 length,
494 hashes: Hashes {
495 sha256: Decoded::from(digest.finish().as_ref().to_vec()),
496 _extra: HashMap::new(),
497 },
498 custom: HashMap::new(),
499 _extra: HashMap::new(),
500 })
501 }
502}
503
504impl Targets {
505 /// Create a new `Targets` object.
506 pub fn new(spec_version: String, version: NonZeroU64, expires: jiff::Timestamp) -> Self {
507 Targets {
508 spec_version,
509 version,
510 expires,
511 targets: HashMap::new(),
512 _extra: HashMap::new(),
513 delegations: Some(Delegations::new()),
514 }
515 }
516
517 /// Given a target url, returns a reference to the Target struct or error if the target is
518 /// unreachable.
519 ///
520 /// **Caution**: does not imply that delegations in this struct or any child are valid.
521 ///
522 pub fn find_target(&self, target_name: &TargetName, permissive: bool) -> Result<&Target> {
523 // visited roles: specification 5.6.7.1 only visit a role once while searching
524 // This breaks any cyclic delegation, and speeds up searches with redundant
525 // delegation.
526 let mut visited: BTreeSet<String> = BTreeSet::new();
527 // terminated: set true when a terminating delegation is selected. No
528 // subsequent delegations should be consulted.
529 let mut terminated = false;
530 self.find_target_from_role(target_name, &mut visited, &mut terminated, permissive)
531 }
532
533 fn find_target_from_role(
534 &self,
535 target_name: &TargetName,
536 visited: &mut BTreeSet<String>,
537 terminated: &mut bool,
538 permissive: bool,
539 ) -> Result<&Target> {
540 if let Some(target) = self.targets.get(target_name) {
541 return Ok(target);
542 }
543 if let Some(delegations) = &self.delegations {
544 for role in &delegations.roles {
545 // If the target cannot match this DelegatedRole, then we do not want to recurse and
546 // check any of its child roles either. If we have already visited this role, we
547 // do not need to visit it again.
548 if !role.paths.matches_target_name(target_name) || visited.contains(&role.name) {
549 continue;
550 }
551 visited.insert(role.name.clone());
552 if let Some(targets) = &role.targets {
553 if let Ok(target) = targets.signed.find_target_from_role(
554 target_name,
555 visited,
556 terminated,
557 permissive,
558 ) {
559 return Ok(target);
560 }
561 if !permissive && *terminated {
562 // we encountered a terminating delegation, so we stop iterating immediately
563 break;
564 }
565 }
566 if role.terminating && !permissive {
567 // this role was terminating, so set terminated. This will cause all ancestors
568 // to stop iterating and return not-found.
569 *terminated = true;
570 break;
571 }
572 }
573 }
574 error::TargetNotFoundSnafu {
575 name: target_name.clone(),
576 }
577 .fail()
578 }
579
580 /// Returns a hashmap of all targets and all delegated targets recursively
581 pub fn targets_map(&self) -> HashMap<TargetName, &Target> {
582 self.targets_iter()
583 .map(|(target_name, target)| (target_name.clone(), target))
584 .collect()
585 }
586
587 /// Returns an iterator of all targets and all delegated targets recursively
588 pub fn targets_iter(&self) -> impl Iterator<Item = (&TargetName, &Target)> + '_ {
589 let mut iter: Box<dyn Iterator<Item = (&TargetName, &Target)>> =
590 Box::new(self.targets.iter());
591 if let Some(delegations) = &self.delegations {
592 for role in &delegations.roles {
593 if let Some(targets) = &role.targets {
594 iter = Box::new(iter.chain(targets.signed.targets_iter()));
595 }
596 }
597 }
598 iter
599 }
600
601 /// Recursively clears all targets
602 pub fn clear_targets(&mut self) {
603 self.targets = HashMap::new();
604 if let Some(delegations) = &mut self.delegations {
605 for delegated_role in &mut delegations.roles {
606 if let Some(targets) = &mut delegated_role.targets {
607 targets.signed.clear_targets();
608 }
609 }
610 }
611 }
612
613 /// Add a target to targets
614 pub fn add_target(&mut self, name: TargetName, target: Target) {
615 self.targets.insert(name, target);
616 }
617
618 /// Remove a target from targets
619 pub fn remove_target(&mut self, name: &TargetName) -> Option<Target> {
620 self.targets.remove(name)
621 }
622
623 /// Returns the `&Signed<Targets>` for `name`
624 pub fn delegated_targets(&self, name: &str) -> Result<&Signed<Targets>> {
625 self.delegated_role(name)?
626 .targets
627 .as_ref()
628 .ok_or(error::Error::NoTargets)
629 }
630
631 /// Returns a mutable `Signed<Targets>` for `name`
632 pub fn delegated_targets_mut(&mut self, name: &str) -> Result<&mut Signed<Targets>> {
633 self.delegated_role_mut(name)?
634 .targets
635 .as_mut()
636 .ok_or(error::Error::NoTargets)
637 }
638
639 /// Returns the `&DelegatedRole` for `name`
640 pub fn delegated_role(&self, name: &str) -> Result<&DelegatedRole> {
641 for role in &self
642 .delegations
643 .as_ref()
644 .ok_or(error::Error::NoDelegations)?
645 .roles
646 {
647 if role.name == name {
648 return Ok(role);
649 } else if let Ok(role) = role
650 .targets
651 .as_ref()
652 .ok_or(error::Error::NoTargets)?
653 .signed
654 .delegated_role(name)
655 {
656 return Ok(role);
657 }
658 }
659 Err(error::Error::RoleNotFound {
660 name: name.to_string(),
661 })
662 }
663
664 /// Returns a mutable `DelegatedRole` for `name`
665 pub fn delegated_role_mut(&mut self, name: &str) -> Result<&mut DelegatedRole> {
666 for role in &mut self
667 .delegations
668 .as_mut()
669 .ok_or(error::Error::NoDelegations)?
670 .roles
671 {
672 if role.name == name {
673 return Ok(role);
674 } else if let Ok(role) = role
675 .targets
676 .as_mut()
677 .ok_or(error::Error::NoTargets)?
678 .signed
679 .delegated_role_mut(name)
680 {
681 return Ok(role);
682 }
683 }
684 Err(error::Error::RoleNotFound {
685 name: name.to_string(),
686 })
687 }
688
689 ///Returns a vec of all rolenames
690 pub fn role_names(&self) -> Vec<&String> {
691 let mut roles = Vec::new();
692 if let Some(delelegations) = &self.delegations {
693 for role in &delelegations.roles {
694 roles.push(&role.name);
695 if let Some(targets) = &role.targets {
696 roles.append(&mut targets.signed.role_names());
697 }
698 }
699 }
700
701 roles
702 }
703
704 /// Returns a reference to the parent delegation of `name`
705 pub fn parent_of(&self, name: &str) -> Result<&Delegations> {
706 if let Some(delegations) = &self.delegations {
707 for role in &delegations.roles {
708 if role.name == name {
709 return Ok(delegations);
710 }
711 if let Some(targets) = &role.targets {
712 if let Ok(delegation) = targets.signed.parent_of(name) {
713 return Ok(delegation);
714 }
715 }
716 }
717 }
718 Err(error::Error::RoleNotFound {
719 name: name.to_string(),
720 })
721 }
722
723 /// Returns a vec of all targets roles delegated by this role
724 pub fn signed_delegated_targets(&self) -> Vec<Signed<DelegatedTargets>> {
725 let mut delegated_targets = Vec::new();
726 if let Some(delegations) = &self.delegations {
727 for role in &delegations.roles {
728 if let Some(targets) = &role.targets {
729 delegated_targets.push(targets.clone().delegated_targets(&role.name));
730 delegated_targets.extend(targets.signed.signed_delegated_targets());
731 }
732 }
733 }
734 delegated_targets
735 }
736
737 /// Link all current targets to `new_targets` metadata, returns a list of new `Targets` not included in the original `Targets`' delegated roles
738 /// This is used to insert a set of updated `Targets` metadata without reloading the rest of the chain.
739 pub fn update_targets(&self, new_targets: &mut Signed<Targets>) -> Vec<String> {
740 let mut needed_roles = Vec::new();
741 // Copy existing targets into proper places of new_targets
742 if let Some(delegations) = &mut new_targets.signed.delegations {
743 for role in &mut delegations.roles {
744 // Check to see if `role.name` has already been loaded
745 if let Ok(targets) = self.delegated_targets(&role.name) {
746 // If it has been loaded, use it as the targets for the role
747 role.targets = Some(targets.clone());
748 } else {
749 // If not make sure we keep track that it needs to be loaded
750 needed_roles.push(role.name.clone());
751 }
752 }
753 }
754
755 needed_roles
756 }
757
758 /// Calls `find_target` on each target (recursively provided by `targets_iter`). This
759 /// proves that the target is either owned by us, or correctly matches through some hierarchy of
760 /// [`PathSets`] below us. When called on the top level [`Targets`] of a repository, this proves
761 /// that the ownership of each target is valid.
762 pub(crate) fn validate(&self) -> Result<()> {
763 for (target_name, _) in self.targets_iter() {
764 self.find_target(target_name, true)?;
765 }
766 Ok(())
767 }
768}
769
770impl Role for Targets {
771 const TYPE: RoleType = RoleType::Targets;
772
773 fn expires(&self) -> jiff::Timestamp {
774 self.expires
775 }
776
777 fn version(&self) -> NonZeroU64 {
778 self.version
779 }
780
781 fn filename(&self, consistent_snapshot: bool) -> String {
782 if consistent_snapshot {
783 format!("{}.targets.json", self.version())
784 } else {
785 "targets.json".to_string()
786 }
787 }
788}
789
790/// Wrapper for `Targets` so that a `Targets` role can be given a name
791#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
792pub struct DelegatedTargets {
793 /// The name of the role
794 #[serde(skip)]
795 pub name: String,
796 /// The targets representing the role metadata
797 #[serde(flatten)]
798 pub targets: Targets,
799}
800
801impl Deref for DelegatedTargets {
802 type Target = Targets;
803
804 fn deref(&self) -> &Targets {
805 &self.targets
806 }
807}
808
809impl DerefMut for DelegatedTargets {
810 fn deref_mut(&mut self) -> &mut Targets {
811 &mut self.targets
812 }
813}
814
815impl Role for DelegatedTargets {
816 const TYPE: RoleType = RoleType::DelegatedTargets;
817
818 fn expires(&self) -> jiff::Timestamp {
819 self.targets.expires
820 }
821
822 fn version(&self) -> NonZeroU64 {
823 self.targets.version
824 }
825
826 fn filename(&self, consistent_snapshot: bool) -> String {
827 if consistent_snapshot {
828 format!("{}.{}.json", self.version(), encode_filename(&self.name))
829 } else {
830 format!("{}.json", encode_filename(&self.name))
831 }
832 }
833
834 fn role_id(&self) -> RoleId {
835 if self.name == "targets" {
836 RoleId::StandardRole(RoleType::Targets)
837 } else {
838 RoleId::DelegatedRole(self.name.clone())
839 }
840 }
841}
842
843impl Signed<DelegatedTargets> {
844 /// Convert a `Signed<DelegatedTargets>` to the string representing the role and its `Signed<Targets>`
845 pub fn targets(self) -> (String, Signed<Targets>) {
846 (
847 self.signed.name,
848 Signed {
849 signed: self.signed.targets,
850 signatures: self.signatures,
851 },
852 )
853 }
854}
855
856impl Signed<Targets> {
857 /// Use a string and a `Signed<Targets>` to create a `Signed<DelegatedTargets>`
858 pub fn delegated_targets(self, name: &str) -> Signed<DelegatedTargets> {
859 Signed {
860 signed: DelegatedTargets {
861 name: name.to_string(),
862 targets: self.signed,
863 },
864 signatures: self.signatures,
865 }
866 }
867}
868
869/// Delegations are found in a `targets.json` file.
870/// TUF 4.5: DELEGATIONS is an object whose format is the following:
871/// ```text
872/// { "keys" : {
873/// KEYID : KEY,
874/// ... },
875/// "roles" : [{
876/// "name": ROLENAME,
877/// "keyids" : [ KEYID, ... ] ,
878/// "threshold" : THRESHOLD,
879/// ("path_hash_prefixes" : [ HEX_DIGEST, ... ] |
880/// "paths" : [ PATHPATTERN, ... ]),
881/// "terminating": TERMINATING,
882/// }, ... ]
883/// }
884/// ```
885#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
886pub struct Delegations {
887 /// Lists the public keys to verify signatures of delegated targets roles. Revocation and
888 /// replacement of delegated targets roles keys is done by changing the keys in this field in
889 /// the delegating role's metadata.
890 #[serde(deserialize_with = "de::deserialize_keys")]
891 pub keys: HashMap<Decoded<Hex>, Key>,
892
893 /// The list of delegated roles.
894 pub roles: Vec<DelegatedRole>,
895}
896
897/// Each role delegated in a targets file is considered a delegated role
898#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
899pub struct DelegatedRole {
900 /// The name of the delegated role. For example, "projects".
901 pub name: String,
902
903 /// The key IDs used by this role.
904 pub keyids: Vec<Decoded<Hex>>,
905
906 /// The threshold of signatures required to validate the role.
907 pub threshold: NonZeroU64,
908
909 /// The paths governed by this role.
910 #[serde(flatten)]
911 pub paths: PathSet,
912
913 /// Indicates whether subsequent delegations should be considered.
914 pub terminating: bool,
915
916 /// The targets that are signed by this role.
917 #[serde(skip)]
918 pub targets: Option<Signed<Targets>>,
919}
920
921/// Specifies the target paths that a delegated role controls.
922#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
923pub enum PathSet {
924 /// The "paths" list describes paths that the role is trusted to provide. Clients MUST check
925 /// that a target is in one of the trusted paths of all roles in a delegation chain, not just in
926 /// a trusted path of the role that describes the target file. PATHPATTERN can include shell-
927 /// style wildcards and supports the Unix filename pattern matching convention. Its format may
928 /// either indicate a path to a single file, or to multiple paths with the use of shell-style
929 /// wildcards. For example, the path pattern "targets/*.tgz" would match file paths
930 /// "targets/foo.tgz" and "targets/bar.tgz", but not "targets/foo.txt". Likewise, path pattern
931 /// "foo-version-?.tgz" matches "foo-version-2.tgz" and "foo-version-a.tgz", but not
932 /// "foo-version-alpha.tgz". To avoid surprising behavior when matching targets with
933 /// PATHPATTERN, it is RECOMMENDED that PATHPATTERN uses the forward slash (/) as directory
934 /// separator and does not start with a directory separator, akin to TARGETSPATH.
935 #[serde(rename = "paths")]
936 Paths(Vec<PathPattern>),
937
938 /// The `path_hash_prefixes` list is used to succinctly describe a set of target paths.
939 /// Specifically, each `HEX_DIGEST` in `path_hash_prefixes` describes a set of target paths;
940 /// therefore, `path_hash_prefixes` is the union over each prefix of its set of target paths.
941 /// The target paths must meet this condition: each target path, when hashed with the SHA-256
942 /// hash function to produce a 64-byte hexadecimal digest (`HEX_DIGEST`), must share the same
943 /// prefix as one of the prefixes in `path_hash_prefixes`. This is useful to split a large
944 /// number of targets into separate bins identified by consistent hashing.
945 #[serde(rename = "path_hash_prefixes")]
946 PathHashPrefixes(Vec<PathHashPrefix>),
947}
948
949/// A glob-like path pattern for matching delegated targets, e.g. `foo/bar/*`.
950///
951/// `PATHPATTERN` supports the Unix shell pattern matching convention for paths
952/// ([glob](https://man7.org/linux/man-pages/man7/glob.7.html)bing pathnames). Its format may either
953/// indicate a path to a single file, or to multiple files with the use of shell-style wildcards
954/// (`*` or `?`). To avoid surprising behavior when matching targets with `PATHPATTERN` it is
955/// RECOMMENDED that `PATHPATTERN` uses the forward slash (`/`) as directory separator and does
956/// not start with a directory separator, as is also recommended for `TARGETPATH`. A path
957/// separator in a path SHOULD NOT be matched by a wildcard in the `PATHPATTERN`.
958///
959/// Some example `PATHPATTERN`s and expected matches:
960/// * a `PATHPATTERN` of `"targets/*.tgz"` would match file paths `"targets/foo.tgz"` and
961/// `"targets/bar.tgz"`, but not `"targets/foo.txt"`.
962/// * a `PATHPATTERN` of `"foo-version-?.tgz"` matches `"foo-version-2.tgz"` and
963/// `"foo-version-a.tgz"`, but not `"foo-version-alpha.tgz"`.
964/// * a `PATHPATTERN` of `"*.tgz"` would match `"foo.tgz"` and `"bar.tgz"`,
965/// but not `"targets/foo.tgz"`
966/// * a `PATHPATTERN` of `"foo.tgz"` would match only `"foo.tgz"`
967#[derive(Clone, Debug)]
968pub struct PathPattern {
969 value: String,
970 glob: GlobMatcher,
971}
972
973impl PathPattern {
974 /// Create a new, valid `PathPattern`. This will fail if we cannot parse the value as a glob. It is important that
975 /// our implementation stop if it encounters a glob it cannot parse so that we do not load repositories where we
976 /// cannot enforce delegate ownership.
977 pub fn new<S: Into<String>>(value: S) -> Result<Self> {
978 let value = value.into();
979 let glob = Glob::new(&value)
980 .context(error::GlobSnafu { pattern: &value })?
981 .compile_matcher();
982 Ok(Self { value, glob })
983 }
984
985 /// Get the inner value of this `PathPattern` as a string.
986 pub fn value(&self) -> &str {
987 &self.value
988 }
989
990 fn matches_target_name(&self, target_name: &TargetName) -> bool {
991 self.glob.is_match(target_name.resolved())
992 }
993}
994
995impl FromStr for PathPattern {
996 type Err = Error;
997
998 fn from_str(s: &str) -> Result<Self> {
999 PathPattern::new(s)
1000 }
1001}
1002
1003impl PartialEq for PathPattern {
1004 fn eq(&self, other: &Self) -> bool {
1005 PartialEq::eq(&self.value, &other.value)
1006 }
1007}
1008
1009impl Serialize for PathPattern {
1010 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1011 where
1012 S: Serializer,
1013 {
1014 serializer.serialize_str(self.value().as_ref())
1015 }
1016}
1017
1018impl<'de> Deserialize<'de> for PathPattern {
1019 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1020 where
1021 D: Deserializer<'de>,
1022 {
1023 let s = <String>::deserialize(deserializer)?;
1024 PathPattern::new(s).map_err(|e| D::Error::custom(format!("{e}")))
1025 }
1026}
1027
1028/// The first characters found in the string representation of a sha256 digest. This can be used for
1029/// randomly sharding a repository. See [`PathSet::PathHashDigest`] for the description of how this
1030/// is used.
1031#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
1032pub struct PathHashPrefix(String);
1033
1034impl PathHashPrefix {
1035 /// Create a new, valid `PathPattern`.
1036 pub fn new<S: Into<String>>(value: S) -> Result<Self> {
1037 // In case we choose to reject some of these in the future, we return a result. For now this
1038 // will always succeed.
1039 Ok(PathHashPrefix(value.into()))
1040 }
1041
1042 /// Get the inner value of this `PathPattern` as a string.
1043 pub fn value(&self) -> &str {
1044 &self.0
1045 }
1046
1047 fn matches_target_name(&self, target_name: &TargetName) -> bool {
1048 let target_name_digest =
1049 digest(&SHA256, target_name.resolved().as_bytes()).encode_hex::<String>();
1050 target_name_digest.starts_with(self.value())
1051 }
1052}
1053
1054impl FromStr for PathHashPrefix {
1055 type Err = Error;
1056
1057 fn from_str(s: &str) -> Result<Self> {
1058 PathHashPrefix::new(s)
1059 }
1060}
1061
1062impl PathSet {
1063 /// Given a `target_name`, returns whether or not this `PathSet` contains a pattern or hash
1064 /// prefix that matches.
1065 fn matches_target_name(&self, target_name: &TargetName) -> bool {
1066 match self {
1067 Self::Paths(paths) => {
1068 for path in paths {
1069 if path.matches_target_name(target_name) {
1070 return true;
1071 }
1072 }
1073 }
1074
1075 Self::PathHashPrefixes(path_prefixes) => {
1076 for prefix in path_prefixes {
1077 if prefix.matches_target_name(target_name) {
1078 return true;
1079 }
1080 }
1081 }
1082 }
1083 false
1084 }
1085}
1086
1087impl Delegations {
1088 /// Creates a new Delegations with no keys or roles
1089 pub fn new() -> Self {
1090 Delegations {
1091 keys: HashMap::new(),
1092 roles: Vec::new(),
1093 }
1094 }
1095
1096 /// Determines if target passes pathset specific matching
1097 pub fn target_is_delegated(&self, target: &TargetName) -> bool {
1098 for role in &self.roles {
1099 if role.paths.matches_target_name(target) {
1100 return true;
1101 }
1102 }
1103 false
1104 }
1105
1106 /// Given an object/key that impls Sign, return the corresponding
1107 /// key ID from Delegation
1108 pub fn key_id(&self, key_pair: &dyn Sign) -> Option<Decoded<Hex>> {
1109 for (key_id, key) in &self.keys {
1110 if key_pair.tuf_key() == *key {
1111 return Some(key_id.clone());
1112 }
1113 }
1114 None
1115 }
1116}
1117
1118impl DelegatedRole {
1119 /// Returns a `RoleKeys` representation of the role
1120 pub fn keys(&self) -> RoleKeys {
1121 RoleKeys {
1122 keyids: self.keyids.clone(),
1123 threshold: self.threshold,
1124 _extra: HashMap::new(),
1125 }
1126 }
1127}
1128
1129// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
1130
1131/// Represents a `timestamp.json` file.
1132/// TUF 4.6: The timestamp file is signed by a timestamp key. It indicates the latest version of the
1133/// snapshot metadata and is frequently resigned to limit the amount of time a client can be kept
1134/// unaware of interference with obtaining updates.
1135#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
1136#[serde(tag = "_type")]
1137#[serde(rename = "timestamp")]
1138pub struct Timestamp {
1139 /// A string that contains the version number of the TUF specification. Its format follows the
1140 /// Semantic Versioning 2.0.0 (semver) specification.
1141 pub spec_version: String,
1142
1143 /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version
1144 /// number less than the one currently trusted.
1145 pub version: NonZeroU64,
1146
1147 /// Determines when metadata should be considered expired and no longer trusted by clients.
1148 #[serde(serialize_with = "ser::serialize_timestamp")]
1149 pub expires: jiff::Timestamp,
1150
1151 /// METAFILES is the same as described for the snapshot.json file. In the case of the
1152 /// timestamp.json file, this MUST only include a description of the snapshot.json file.
1153 pub meta: HashMap<String, Metafile>,
1154
1155 /// Extra arguments found during deserialization.
1156 ///
1157 /// We must store these to correctly verify signatures for this object.
1158 ///
1159 /// If you're instantiating this struct, you should make this `HashMap::empty()`.
1160 #[serde(flatten)]
1161 #[serde(deserialize_with = "de::extra_skip_type")]
1162 pub _extra: HashMap<String, Value>,
1163}
1164
1165impl Timestamp {
1166 /// Creates a new `Timestamp` object.
1167 pub fn new(spec_version: String, version: NonZeroU64, expires: jiff::Timestamp) -> Self {
1168 Timestamp {
1169 spec_version,
1170 version,
1171 expires,
1172 meta: HashMap::new(),
1173 _extra: HashMap::new(),
1174 }
1175 }
1176}
1177
1178impl Role for Timestamp {
1179 const TYPE: RoleType = RoleType::Timestamp;
1180
1181 fn expires(&self) -> jiff::Timestamp {
1182 self.expires
1183 }
1184
1185 fn version(&self) -> NonZeroU64 {
1186 self.version
1187 }
1188
1189 fn filename(&self, _consistent_snapshot: bool) -> String {
1190 "timestamp.json".to_string()
1191 }
1192}
1193
1194#[test]
1195fn targets_iter_and_map_test() {
1196 use maplit::hashmap;
1197
1198 // Create a dummy Target object.
1199 let nothing = Target {
1200 length: 0,
1201 hashes: Hashes {
1202 sha256: [0u8].to_vec().into(),
1203 _extra: HashMap::default(),
1204 },
1205 custom: HashMap::default(),
1206 _extra: HashMap::default(),
1207 };
1208
1209 // Create a hierarchy of targets/delegations: a -> b -> c
1210 let c_role = DelegatedRole {
1211 name: "c-role".to_string(),
1212 keyids: vec![],
1213 threshold: NonZeroU64::new(1).unwrap(),
1214 paths: PathSet::Paths(vec![PathPattern::new("*").unwrap()]),
1215 terminating: false,
1216 targets: Some(Signed {
1217 signed: Targets {
1218 spec_version: String::new(),
1219 version: NonZeroU64::new(1).unwrap(),
1220 expires: jiff::Timestamp::now(),
1221 targets: hashmap! {
1222 TargetName::new("c.txt").unwrap() => nothing.clone(),
1223 },
1224 delegations: None,
1225 _extra: HashMap::default(),
1226 },
1227 signatures: vec![],
1228 }),
1229 };
1230 let b_delegations = Delegations {
1231 keys: HashMap::default(),
1232 roles: vec![c_role],
1233 };
1234 let b_role = DelegatedRole {
1235 name: "b-role".to_string(),
1236 keyids: vec![],
1237 threshold: NonZeroU64::new(1).unwrap(),
1238 paths: PathSet::Paths(vec![PathPattern::new("*").unwrap()]),
1239 terminating: false,
1240 targets: Some(Signed {
1241 signed: Targets {
1242 spec_version: String::new(),
1243 version: NonZeroU64::new(1).unwrap(),
1244 expires: jiff::Timestamp::now(),
1245 targets: hashmap! {
1246 TargetName::new("b.txt").unwrap() => nothing.clone(),
1247 },
1248 delegations: Some(b_delegations),
1249 _extra: HashMap::default(),
1250 },
1251 signatures: vec![],
1252 }),
1253 };
1254 let a_delegations = Delegations {
1255 keys: HashMap::default(),
1256 roles: vec![b_role],
1257 };
1258 let a = Targets {
1259 spec_version: String::new(),
1260 version: NonZeroU64::new(1).unwrap(),
1261 expires: jiff::Timestamp::now(),
1262 targets: hashmap! {
1263 TargetName::new("a.txt").unwrap() => nothing,
1264 },
1265 delegations: Some(a_delegations),
1266 _extra: HashMap::default(),
1267 };
1268
1269 // Assert that targets_iter is recursive and thus has a.txt, b.txt and c.txt
1270 assert!(a
1271 .targets_iter()
1272 .map(|(key, _)| key)
1273 .any(|item| item.raw() == "a.txt"));
1274 assert!(a
1275 .targets_iter()
1276 .map(|(key, _)| key)
1277 .any(|item| item.raw() == "b.txt"));
1278 assert!(a
1279 .targets_iter()
1280 .map(|(key, _)| key)
1281 .any(|item| item.raw() == "c.txt"));
1282
1283 // Assert that targets_map is also recursive
1284 let map = a.targets_map();
1285 assert!(map.contains_key(&TargetName::new("a.txt").unwrap()));
1286 assert!(map.contains_key(&TargetName::new("b.txt").unwrap()));
1287 assert!(map.contains_key(&TargetName::new("c.txt").unwrap()));
1288}