diff --git a/tuf/src/metadata.rs b/tuf/src/metadata.rs index f8044f4..b215dd2 100644 --- a/tuf/src/metadata.rs +++ b/tuf/src/metadata.rs @@ -718,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(), ) } @@ -766,6 +767,7 @@ pub struct RootMetadata { snapshot: RoleDefinition, targets: RoleDefinition, timestamp: RoleDefinition, + additional_fields: HashMap, } impl RootMetadata { @@ -779,6 +781,7 @@ impl RootMetadata { snapshot: RoleDefinition, targets: RoleDefinition, timestamp: RoleDefinition, + additional_fields: HashMap, ) -> Result { if version < 1 { return Err(Error::MetadataVersionMustBeGreaterThanZero( @@ -795,6 +798,7 @@ impl RootMetadata { snapshot, targets, timestamp, + additional_fields, }) } @@ -860,6 +864,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 { @@ -1125,7 +1134,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`. @@ -1146,6 +1160,7 @@ pub struct TimestampMetadata { version: u32, expires: DateTime, snapshot: MetadataDescription, + additional_fields: HashMap, } impl TimestampMetadata { @@ -1154,6 +1169,7 @@ impl TimestampMetadata { version: u32, expires: DateTime, snapshot: MetadataDescription, + additional_fields: HashMap, ) -> Result { if version < 1 { return Err(Error::MetadataVersionMustBeGreaterThanZero( @@ -1165,6 +1181,7 @@ impl TimestampMetadata { version, expires, snapshot, + additional_fields, }) } @@ -1172,6 +1189,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 { @@ -1383,7 +1405,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`. @@ -1420,6 +1442,7 @@ pub struct SnapshotMetadata { version: u32, expires: DateTime, meta: HashMap>, + additional_fields: HashMap, } impl SnapshotMetadata { @@ -1428,6 +1451,7 @@ impl SnapshotMetadata { version: u32, expires: DateTime, meta: HashMap>, + additional_fields: HashMap, ) -> Result { if version < 1 { return Err(Error::MetadataVersionMustBeGreaterThanZero( @@ -1439,6 +1463,7 @@ impl SnapshotMetadata { version, expires, meta, + additional_fields, }) } @@ -1446,6 +1471,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 { @@ -1844,6 +1874,7 @@ pub struct TargetsMetadata { expires: DateTime, targets: HashMap, delegations: Delegations, + additional_fields: HashMap, } impl TargetsMetadata { @@ -1853,6 +1884,7 @@ impl TargetsMetadata { expires: DateTime, targets: HashMap, delegations: Delegations, + additional_fields: HashMap, ) -> Result { if version < 1 { return Err(Error::MetadataVersionMustBeGreaterThanZero( @@ -1865,6 +1897,7 @@ impl TargetsMetadata { expires, targets, delegations, + additional_fields, }) } @@ -1877,6 +1910,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 { @@ -1993,6 +2031,7 @@ impl TargetsMetadataBuilder { self.expires, self.targets, self.delegations.unwrap_or_default(), + Default::default(), ) } @@ -2544,6 +2583,61 @@ mod test { assert_eq!(decoded, root); } + #[test] + fn serde_root_metadata_additional_fields() { + let jsn = json!({ + "_type": "root", + "spec_version": "1.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "consistent_snapshot": true, + "keys": { + "09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": "1410ae3053aa70bbfa98428a879d64d3002a3578f7dfaaeb1cb0764e860f7e0b", + }, + }, + }, + "roles": { + "root": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"], + }, + "snapshot": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"], + }, + "targets": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"], + }, + "timestamp": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"], + }, + }, + // additional_fields + "custom": { + "foo": 42, + "bar": "baz", + }, + "quux": true, + }); + + let root: RootMetadata = serde_json::from_value(jsn.clone()).unwrap(); + assert_eq!( + root.additional_fields()["custom"], + json!({"foo": 42, "bar": "baz"}) + ); + assert_eq!(root.additional_fields()["quux"], json!(true)); + + // make sure additional_fields are passed through serialization as well + assert_eq!(jsn, serde_json::to_value(&root).unwrap()); + } + fn jsn_root_metadata_without_keyid_hash_algos() -> serde_json::Value { json!({ "_type": "root", @@ -2846,6 +2940,41 @@ mod test { assert_eq!(decoded, timestamp); } + #[test] + fn serde_timestamp_metadata_additional_fields() { + let jsn = json!({ + "_type": "timestamp", + "spec_version": "1.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "", + }, + }, + }, + // additional_fields + "custom": { + "foo": 42, + "bar": "baz", + }, + "quux": true, + }); + + let timestamp: TimestampMetadata = serde_json::from_value(jsn.clone()).unwrap(); + assert_eq!( + timestamp.additional_fields()["custom"], + json!({"foo": 42, "bar": "baz"}) + ); + assert_eq!(timestamp.additional_fields()["quux"], json!(true)); + + // make sure additional_fields are passed through serialization as well + assert_eq!(jsn, serde_json::to_value(×tamp).unwrap()); + } + // Deserialize timestamp metadata with optional length and hashes #[test] fn serde_timestamp_metadata_without_length_and_hashes() { @@ -2865,7 +2994,7 @@ mod test { "snapshot.json": { "version": 1 }, - } + }, }); let encoded = serde_json::to_value(×tamp).unwrap(); @@ -2960,6 +3089,41 @@ mod test { assert_eq!(decoded, snapshot); } + #[test] + fn serde_snapshot_metadata_additional_fields() { + let jsn = json!({ + "_type": "snapshot", + "spec_version": "1.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "targets.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "", + }, + }, + }, + // additional_fields + "custom": { + "foo": 42, + "bar": "baz", + }, + "quux": true, + }); + + let snapshot: SnapshotMetadata = serde_json::from_value(jsn.clone()).unwrap(); + assert_eq!( + snapshot.additional_fields()["custom"], + json!({"foo": 42, "bar": "baz"}) + ); + assert_eq!(snapshot.additional_fields()["quux"], json!(true)); + + // make sure additional_fields are passed through serialization as well + assert_eq!(jsn, serde_json::to_value(&snapshot).unwrap()); + } + // Deserialize snapshot metadata with optional length and hashes #[test] fn serde_snapshot_optional_length_and_hashes() { @@ -3081,6 +3245,66 @@ mod test { }) } + #[test] + fn serde_targets_metadata_additional_fields() { + let jsn = json!({ + "_type": "targets", + "spec_version": "1.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "targets": { + "insert-target-from-slice": { + "length": 3, + "hashes": { + "sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483\ + bfa0f98a5e886266e7ae", + }, + }, + "insert-target-description-from-slice-with-custom": { + "length": 3, + "hashes": { + "sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483\ + bfa0f98a5e886266e7ae", + }, + }, + "insert-target-from-reader": { + "length": 3, + "hashes": { + "sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483\ + bfa0f98a5e886266e7ae", + }, + }, + "insert-target-description-from-reader-with-custom": { + "length": 3, + "hashes": { + "sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483\ + bfa0f98a5e886266e7ae", + }, + "custom": { + "foo": 1, + "bar": "baz", + }, + }, + }, + // additional_fields + "custom": { + "foo": 42, + "bar": "baz", + }, + "quux": true, + }); + + let targets: TargetsMetadata = serde_json::from_value(jsn.clone()).unwrap(); + assert_eq!( + targets.additional_fields()["custom"], + json!({"foo": 42, "bar": "baz"}) + ); + assert_eq!(targets.additional_fields()["quux"], json!(true)); + + // make sure additional_fields are passed through serialization as well + assert_eq!(jsn, serde_json::to_value(&targets).unwrap()); + } + #[test] fn serde_targets_with_delegations_metadata() { let key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); @@ -3260,6 +3484,7 @@ mod test { Utc.ymd(2038, 1, 1).and_hms(0, 0, 0), hashmap!(), Delegations::default(), + Default::default(), ) .unwrap(); diff --git a/tuf/src/pouf/pouf1/shims.rs b/tuf/src/pouf/pouf1/shims.rs index 3619372..103f620 100644 --- a/tuf/src/pouf/pouf1/shims.rs +++ b/tuf/src/pouf/pouf1/shims.rs @@ -52,6 +52,8 @@ pub struct RootMetadata { #[serde(deserialize_with = "deserialize_reject_duplicates::deserialize")] keys: BTreeMap, roles: RoleDefinitions, + #[serde(flatten)] + additional_fields: BTreeMap, } impl RootMetadata { @@ -73,6 +75,7 @@ impl RootMetadata { targets: meta.targets().clone(), timestamp: meta.timestamp().clone(), }, + additional_fields: meta.additional_fields().clone().into_iter().collect(), }) } @@ -109,6 +112,7 @@ impl RootMetadata { self.roles.snapshot, self.roles.targets, self.roles.timestamp, + self.additional_fields.into_iter().collect(), ) } } @@ -172,6 +176,8 @@ pub struct TimestampMetadata { version: u32, expires: String, meta: TimestampMeta, + #[serde(flatten)] + additional_fields: BTreeMap, } #[derive(Serialize, Deserialize)] @@ -191,6 +197,7 @@ impl TimestampMetadata { meta: TimestampMeta { snapshot: metadata.snapshot().clone(), }, + additional_fields: metadata.additional_fields().clone().into_iter().collect(), }) } @@ -213,6 +220,7 @@ impl TimestampMetadata { self.version, parse_datetime(&self.expires)?, self.meta.snapshot, + self.additional_fields.into_iter().collect(), ) } } @@ -226,6 +234,8 @@ pub struct SnapshotMetadata { expires: String, #[serde(deserialize_with = "deserialize_reject_duplicates::deserialize")] meta: BTreeMap>, + #[serde(flatten)] + additional_fields: BTreeMap, } impl SnapshotMetadata { @@ -240,6 +250,7 @@ impl SnapshotMetadata { .iter() .map(|(p, d)| (format!("{}.json", p), d.clone())) .collect(), + additional_fields: metadata.additional_fields().clone().into_iter().collect(), }) } @@ -277,6 +288,7 @@ impl SnapshotMetadata { Ok((p, d)) }) .collect::>()?, + self.additional_fields.into_iter().collect(), ) } } @@ -291,6 +303,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 { @@ -306,6 +320,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(), }) } @@ -329,6 +348,7 @@ impl TargetsMetadata { parse_datetime(&self.expires)?, self.targets.into_iter().collect(), self.delegations, + self.additional_fields.into_iter().collect(), ) } }