diff --git a/tuf/src/interchange/cjson/shims.rs b/tuf/src/interchange/cjson/shims.rs index 9b125e9..f9a502d 100644 --- a/tuf/src/interchange/cjson/shims.rs +++ b/tuf/src/interchange/cjson/shims.rs @@ -8,10 +8,19 @@ use crate::error::Error; use crate::metadata::{self, Metadata}; use crate::Result; -const SPEC_VERSION: &str = "1.0"; +const SPEC_VERSION: &str = "1.0.0"; + +// Ensure the given spec version matches our spec version. +// +// We also need to handle the literal "1.0" here, despite that fact that it is not a valid version +// according to the SemVer spec, because it is already baked into some of the old roots. +fn valid_spec_version(other: &str) -> bool { + other == SPEC_VERSION || other == "1.0" +} fn parse_datetime(ts: &str) -> Result> { - Utc.datetime_from_str(ts, "%FT%TZ") + DateTime::parse_from_rfc3339(ts) + .map(|ts| ts.with_timezone(&Utc)) .map_err(|e| Error::Encoding(format!("Can't parse DateTime: {:?}", e))) } @@ -38,6 +47,8 @@ pub struct RootMetadata { #[serde(deserialize_with = "deserialize_reject_duplicates::deserialize")] keys: BTreeMap, roles: RoleDefinitions, + #[serde(flatten)] + additional_fields: BTreeMap, } impl RootMetadata { @@ -59,6 +70,7 @@ impl RootMetadata { targets: meta.targets().clone(), timestamp: meta.timestamp().clone(), }, + additional_fields: meta.additional_fields().clone().into_iter().collect(), }) } @@ -70,7 +82,7 @@ impl RootMetadata { ))); } - if self.spec_version != SPEC_VERSION { + if !valid_spec_version(&self.spec_version) { return Err(Error::Encoding(format!( "Unknown spec version {}", self.spec_version @@ -95,6 +107,7 @@ impl RootMetadata { self.roles.snapshot, self.roles.targets, self.roles.timestamp, + self.additional_fields.into_iter().collect(), ) } } @@ -154,6 +167,8 @@ pub struct TimestampMetadata { version: u32, expires: String, meta: TimestampMeta, + #[serde(flatten)] + additional_fields: BTreeMap, } #[derive(Serialize, Deserialize)] @@ -173,6 +188,7 @@ impl TimestampMetadata { meta: TimestampMeta { snapshot: metadata.snapshot().clone(), }, + additional_fields: metadata.additional_fields().clone().into_iter().collect(), }) } @@ -184,7 +200,7 @@ impl TimestampMetadata { ))); } - if self.spec_version != SPEC_VERSION { + if !valid_spec_version(&self.spec_version) { return Err(Error::Encoding(format!( "Unknown spec version {}", self.spec_version @@ -195,6 +211,7 @@ impl TimestampMetadata { self.version, parse_datetime(&self.expires)?, self.meta.snapshot, + self.additional_fields.into_iter().collect(), ) } } @@ -208,6 +225,8 @@ pub struct SnapshotMetadata { expires: String, #[serde(deserialize_with = "deserialize_reject_duplicates::deserialize")] meta: BTreeMap, + #[serde(flatten)] + additional_fields: BTreeMap, } impl SnapshotMetadata { @@ -222,6 +241,7 @@ impl SnapshotMetadata { .iter() .map(|(p, d)| (format!("{}.json", p), d.clone())) .collect(), + additional_fields: metadata.additional_fields().clone().into_iter().collect(), }) } @@ -233,7 +253,7 @@ impl SnapshotMetadata { ))); } - if self.spec_version != SPEC_VERSION { + if !valid_spec_version(&self.spec_version) { return Err(Error::Encoding(format!( "Unknown spec version {}", self.spec_version @@ -259,6 +279,7 @@ impl SnapshotMetadata { Ok((p, d)) }) .collect::>()?, + self.additional_fields.into_iter().collect(), ) } } @@ -273,6 +294,8 @@ pub struct TargetsMetadata { targets: BTreeMap, #[serde(default, skip_serializing_if = "metadata::Delegations::is_empty")] delegations: metadata::Delegations, + #[serde(flatten)] + additional_fields: BTreeMap, } impl TargetsMetadata { @@ -288,6 +311,11 @@ impl TargetsMetadata { .map(|(p, d)| (p.clone(), d.clone())) .collect(), delegations: metadata.delegations().clone(), + additional_fields: metadata + .additional_fields() + .iter() + .map(|(p, d)| (p.clone(), d.clone())) + .collect(), }) } @@ -299,7 +327,7 @@ impl TargetsMetadata { ))); } - if self.spec_version != SPEC_VERSION { + if !valid_spec_version(&self.spec_version) { return Err(Error::Encoding(format!( "Unknown spec version {}", self.spec_version @@ -311,6 +339,7 @@ impl TargetsMetadata { parse_datetime(&self.expires)?, self.targets.into_iter().collect(), self.delegations, + self.additional_fields.into_iter().collect(), ) } } @@ -570,3 +599,51 @@ mod deserialize_reject_duplicates { }) } } + +#[cfg(test)] +mod test { + use super::{parse_datetime, valid_spec_version}; + + #[test] + fn spec_version_validation() { + let valid_spec_versions = ["1.0.0", "1.0"]; + + for version in valid_spec_versions { + assert!(valid_spec_version(version), "{:?} should be valid", version); + } + + let invalid_spec_versions = ["1.0.1", "1.1.0", "2.0.0", "3.0"]; + + for version in invalid_spec_versions { + assert!( + !valid_spec_version(version), + "{:?} should be invalid", + version + ); + } + } + + #[test] + fn datetime_formats() { + // The TUF spec says datetimes should be in ISO8601 format, specifically + // "YYYY-MM-DDTHH:MM:SSZ". Since not all TUF clients adhere strictly to that, we choose to + // be more lenient here. The following represent the intersection of valid ISO8601 + // and RFC3339 datetime formats (source: https://ijmacd.github.io/rfc3339-iso8601/). + let valid_formats = [ + "2022-08-30T19:53:55Z", + "2022-08-30T19:53:55.7Z", + "2022-08-30T19:53:55.77Z", + "2022-08-30T19:53:55.775Z", + "2022-08-30T19:53:55+00:00", + "2022-08-30T19:53:55.7+00:00", + "2022-08-30T14:53:55-05:00", + "2022-08-30T14:53:55.7-05:00", + "2022-08-30T14:53:55.77-05:00", + "2022-08-30T14:53:55.775-05:00", + ]; + + for format in valid_formats { + assert!(parse_datetime(format).is_ok(), "should parse {:?}", format); + } + } +} diff --git a/tuf/src/interchange/mod.rs b/tuf/src/interchange/mod.rs index e295461..f69b0e5 100644 --- a/tuf/src/interchange/mod.rs +++ b/tuf/src/interchange/mod.rs @@ -10,7 +10,7 @@ use std::fmt::Debug; use crate::Result; /// The format used for data interchange, serialization, and deserialization. -pub trait DataInterchange: Debug + PartialEq + Clone { +pub trait DataInterchange: Debug + PartialEq + Clone + Send { /// The type of data that is contained in the `signed` portion of metadata. type RawData: Serialize + DeserializeOwned + Clone + PartialEq; diff --git a/tuf/src/metadata.rs b/tuf/src/metadata.rs index d8aa7e3..df82a2e 100644 --- a/tuf/src/metadata.rs +++ b/tuf/src/metadata.rs @@ -66,7 +66,6 @@ static PATH_ILLEGAL_STRINGS: &[&str] = &[ "\"", "|", "?", - "*", // control characters, all illegal in FAT "\u{000}", "\u{001}", @@ -719,6 +718,7 @@ impl RootMetadataBuilder { RoleDefinition::new(self.snapshot_threshold, self.snapshot_key_ids)?, RoleDefinition::new(self.targets_threshold, self.targets_key_ids)?, RoleDefinition::new(self.timestamp_threshold, self.timestamp_key_ids)?, + Default::default(), ) } @@ -767,6 +767,7 @@ pub struct RootMetadata { snapshot: RoleDefinition, targets: RoleDefinition, timestamp: RoleDefinition, + additional_fields: HashMap, } impl RootMetadata { @@ -780,6 +781,7 @@ impl RootMetadata { snapshot: RoleDefinition, targets: RoleDefinition, timestamp: RoleDefinition, + additional_fields: HashMap, ) -> Result { if version < 1 { return Err(Error::IllegalArgument(format!( @@ -797,6 +799,7 @@ impl RootMetadata { snapshot, targets, timestamp, + additional_fields, }) } @@ -862,6 +865,11 @@ impl RootMetadata { pub fn timestamp(&self) -> &RoleDefinition { &self.timestamp } + + /// An immutable reference to any additional fields on the metadata. + pub fn additional_fields(&self) -> &HashMap { + &self.additional_fields + } } impl Metadata for RootMetadata { @@ -1128,7 +1136,12 @@ impl TimestampMetadataBuilder { /// Construct a new `TimestampMetadata`. pub fn build(self) -> Result { - TimestampMetadata::new(self.version, self.expires, self.snapshot) + TimestampMetadata::new( + self.version, + self.expires, + self.snapshot, + Default::default(), + ) } /// Construct a new `SignedMetadata`. @@ -1149,6 +1162,7 @@ pub struct TimestampMetadata { version: u32, expires: DateTime, snapshot: MetadataDescription, + additional_fields: HashMap, } impl TimestampMetadata { @@ -1157,6 +1171,7 @@ impl TimestampMetadata { version: u32, expires: DateTime, snapshot: MetadataDescription, + additional_fields: HashMap, ) -> Result { if version < 1 { return Err(Error::IllegalArgument(format!( @@ -1169,6 +1184,7 @@ impl TimestampMetadata { version, expires, snapshot, + additional_fields, }) } @@ -1176,6 +1192,11 @@ impl TimestampMetadata { pub fn snapshot(&self) -> &MetadataDescription { &self.snapshot } + + /// An immutable reference to any additional fields on the metadata. + pub fn additional_fields(&self) -> &HashMap { + &self.additional_fields + } } impl Metadata for TimestampMetadata { @@ -1380,7 +1401,7 @@ impl SnapshotMetadataBuilder { /// Construct a new `SnapshotMetadata`. pub fn build(self) -> Result { - SnapshotMetadata::new(self.version, self.expires, self.meta) + SnapshotMetadata::new(self.version, self.expires, self.meta, Default::default()) } /// Construct a new `SignedMetadata`. @@ -1417,6 +1438,7 @@ pub struct SnapshotMetadata { version: u32, expires: DateTime, meta: HashMap, + additional_fields: HashMap, } impl SnapshotMetadata { @@ -1425,6 +1447,7 @@ impl SnapshotMetadata { version: u32, expires: DateTime, meta: HashMap, + additional_fields: HashMap, ) -> Result { if version < 1 { return Err(Error::IllegalArgument(format!( @@ -1437,6 +1460,7 @@ impl SnapshotMetadata { version, expires, meta, + additional_fields, }) } @@ -1444,6 +1468,11 @@ impl SnapshotMetadata { pub fn meta(&self) -> &HashMap { &self.meta } + + /// An immutable reference to any additional fields on the metadata. + pub fn additional_fields(&self) -> &HashMap { + &self.additional_fields + } } impl Metadata for SnapshotMetadata { @@ -1842,6 +1871,7 @@ pub struct TargetsMetadata { expires: DateTime, targets: HashMap, delegations: Delegations, + additional_fields: HashMap, } impl TargetsMetadata { @@ -1851,6 +1881,7 @@ impl TargetsMetadata { expires: DateTime, targets: HashMap, delegations: Delegations, + additional_fields: HashMap, ) -> Result { if version < 1 { return Err(Error::IllegalArgument(format!( @@ -1864,6 +1895,7 @@ impl TargetsMetadata { expires, targets, delegations, + additional_fields, }) } @@ -1876,6 +1908,11 @@ impl TargetsMetadata { pub fn delegations(&self) -> &Delegations { &self.delegations } + + /// An immutable reference to any additional fields on the metadata. + pub fn additional_fields(&self) -> &HashMap { + &self.additional_fields + } } impl Metadata for TargetsMetadata { @@ -1992,6 +2029,7 @@ impl TargetsMetadataBuilder { self.expires, self.targets, self.delegations.unwrap_or_default(), + Default::default(), ) } @@ -2314,6 +2352,24 @@ mod test { } } + #[test] + fn allow_asterisk_in_target_path() { + let good_paths = &[ + "*", + "*/some/path", + "*/some/path/", + "some/*/path", + "some/*/path/*", + ]; + + for path in good_paths.iter() { + assert!(safe_path(path).is_ok()); + assert!(TargetPath::new(path.to_string()).is_ok()); + assert!(MetadataPath::new(path.to_string()).is_ok()); + assert!(TargetPath::new(path.to_string()).is_ok()); + } + } + #[test] fn path_matches_chain() { let test_cases: &[(bool, &str, &[&[&str]])] = &[ @@ -2462,7 +2518,7 @@ mod test { let jsn = json!({ "_type": "root", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "consistent_snapshot": true, @@ -2529,7 +2585,7 @@ mod test { fn jsn_root_metadata_without_keyid_hash_algos() -> serde_json::Value { json!({ "_type": "root", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "consistent_snapshot": false, @@ -2634,7 +2690,7 @@ mod test { let jsn = json!({ "signatures": [{ "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", - "sig": "c4ba838e0d3f783716393a4d691f568f840733ff488bb79ac68287e97e0b31d63fcef392dbc978e878c2103ba231905af634cc651d6f0e63a35782d051ac6e00" + "sig": "1f944e022d0b30c5a9ddc9c210026f396e18a17cc9a4ee92c339a8ee63357608dba8121847a825c3a5c84c1081435436bd784c8086c3103cdd1489e79cff2802" }], "signed": jsn_root_metadata_without_keyid_hash_algos() }); @@ -2659,11 +2715,11 @@ mod test { let jsn = json!({ "signatures": [{ "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", - "sig": "c4ba838e0d3f783716393a4d691f568f840733ff488bb79ac68287e97e0b31d63fcef392dbc978e878c2103ba231905af634cc651d6f0e63a35782d051ac6e00" + "sig": "1f944e022d0b30c5a9ddc9c210026f396e18a17cc9a4ee92c339a8ee63357608dba8121847a825c3a5c84c1081435436bd784c8086c3103cdd1489e79cff2802" }, { "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", - "sig": "c4ba838e0d3f783716393a4d691f568f840733ff488bb79ac68287e97e0b31d63fcef392dbc978e878c2103ba231905af634cc651d6f0e63a35782d051ac6e00" + "sig": "1f944e022d0b30c5a9ddc9c210026f396e18a17cc9a4ee92c339a8ee63357608dba8121847a825c3a5c84c1081435436bd784c8086c3103cdd1489e79cff2802" }], "signed": jsn_root_metadata_without_keyid_hash_algos() }); @@ -2671,6 +2727,7 @@ mod test { let decoded: SignedMetadata = serde_json::from_value(jsn).unwrap(); let raw_root = decoded.to_raw().unwrap(); + assert_matches!( verify_signatures(&MetadataPath::root(), &raw_root, 2, &[root_key.public().clone()]), Err(Error::MetadataMissingSignatures { @@ -2808,7 +2865,7 @@ mod test { let jsn = json!({ "_type": "timestamp", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": { @@ -2840,7 +2897,7 @@ mod test { let jsn = json!({ "_type": "timestamp", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": { @@ -2860,7 +2917,7 @@ mod test { fn serde_timestamp_metadata_missing_snapshot() { let jsn = json!({ "_type": "timestamp", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": {} @@ -2876,7 +2933,7 @@ mod test { fn serde_timestamp_metadata_extra_metadata() { let jsn = json!({ "_type": "timestamp", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": { @@ -2922,7 +2979,7 @@ mod test { let jsn = json!({ "_type": "snapshot", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": { @@ -2956,7 +3013,7 @@ mod test { let jsn = json!({ "_type": "snapshot", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": { @@ -3017,7 +3074,7 @@ mod test { let jsn = json!({ "_type": "targets", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "targets": { @@ -3087,7 +3144,7 @@ mod test { let jsn = json!({ "_type": "targets", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "targets": {}, @@ -3145,14 +3202,14 @@ mod test { "signatures": [ { "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", - "sig": "ea48ddc7b3ea614b394e508eb8722100f94ff1a4e3aac3af09d\ - a0dada4f878431e8ac26160833405ec239924dfe62edf605fee8294\ - c49b4acade55c76e817602", + "sig": "a9b97b2439cd41e9a8c62e4d2f8f73b25a06095e0a994e8631a0\ + 88977271909af2cc829c68637af98b07ebffeea308cc1a1c83d18fa2\ + 9ec401493973b3dfa90e", } ], "signed": { "_type": "snapshot", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": { @@ -3242,6 +3299,7 @@ mod test { Utc.ymd(2038, 1, 1).and_hms(0, 0, 0), hashmap!(), Delegations::default(), + Default::default(), ) .unwrap(); @@ -3312,7 +3370,7 @@ mod test { fn deserialize_json_root_duplicate_keys() { let root_json = r#"{ "_type": "root", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "consistent_snapshot": false, @@ -3498,7 +3556,7 @@ mod test { fn deserialize_json_snapshot_duplicate_metadata() { let snapshot_json = r#"{ "_type": "snapshot", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": { @@ -3563,7 +3621,7 @@ mod test { fn deserialize_json_timestamp_duplicate_metadata() { let timestamp_json = r#"{ "_type": "timestamp", - "spec_version": "1.0", + "spec_version": "1.0.0", "version": 1, "expires": "2017-01-01T00:00:00Z", "meta": { diff --git a/tuf/src/repository.rs b/tuf/src/repository.rs index a23f1bc..3b314a3 100644 --- a/tuf/src/repository.rs +++ b/tuf/src/repository.rs @@ -108,7 +108,7 @@ where /// A writable TUF repository. Most implementors of this trait should also implement /// `RepositoryProvider`. -pub trait RepositoryStorage +pub trait RepositoryStorage: Send where D: DataInterchange + Sync, {