Skip to content

Commit 7b8af7a

Browse files
committed
Add support for time versioning
Time versioning is a metadata versioning approach where we use the current UTC unix timestamp when creating non-root metadata, rather than monotonically increasing the metadata version. This is useful when working with a test device that may have TUF metadata created from one source, then updated with developer-created metadata. Assuming the user's workstation and test device have their system times in sync, the user can create new TUF metadata that is a newer version than the metadata on the device without needing to query the device to see what it thinks is the latest metadata version.
1 parent c611a6f commit 7b8af7a

File tree

1 file changed

+206
-11
lines changed

1 file changed

+206
-11
lines changed

tuf/src/repo_builder.rs

Lines changed: 206 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ where
219219
trusted_targets_keys: Vec<&'a dyn PrivateKey>,
220220
trusted_snapshot_keys: Vec<&'a dyn PrivateKey>,
221221
trusted_timestamp_keys: Vec<&'a dyn PrivateKey>,
222+
time_version: Option<u32>,
222223
root_expiration_duration: Duration,
223224
targets_expiration_duration: Duration,
224225
snapshot_expiration_duration: Duration,
@@ -289,6 +290,48 @@ where
289290

290291
false
291292
}
293+
294+
/// The initial version number for non-root metadata.
295+
fn non_root_initial_version(&self) -> u32 {
296+
if let Some(time_version) = self.time_version {
297+
time_version
298+
} else {
299+
1
300+
}
301+
}
302+
303+
/// If time versioning is enabled, this updates the current time version to match the current
304+
/// time. It will disable time versioning if the current timestamp is less than or equal to
305+
/// zero, or it is greater than max u32.
306+
fn update_time_version(&mut self) {
307+
// We can use the time version if it is greater than zero and less than max u32. Otherwise
308+
// fall back to default monontonic versioning.
309+
let timestamp = self.current_time.timestamp();
310+
if timestamp > 0 {
311+
self.time_version = timestamp.try_into().ok();
312+
} else {
313+
self.time_version = None;
314+
}
315+
}
316+
317+
/// The next version number for non-root metadata.
318+
fn non_root_next_version(
319+
&self,
320+
current_version: u32,
321+
path: fn() -> MetadataPath,
322+
) -> Result<u32> {
323+
if let Some(time_version) = self.time_version {
324+
// We can only use the time version if it's larger than our current version. If not,
325+
// then fall back to the next version.
326+
if current_version < time_version {
327+
return Ok(time_version);
328+
}
329+
}
330+
331+
current_version
332+
.checked_add(1)
333+
.ok_or_else(|| Error::MetadataVersionMustBeSmallerThanMaxU32(path()))
334+
}
292335
}
293336

294337
fn sign<'a, D, I, M>(meta: &M, keys: I) -> Result<RawSignedMetadata<D, M>>
@@ -367,6 +410,7 @@ where
367410
trusted_targets_keys: vec![],
368411
trusted_snapshot_keys: vec![],
369412
trusted_timestamp_keys: vec![],
413+
time_version: None,
370414
root_expiration_duration: Duration::days(DEFAULT_ROOT_EXPIRATION_DAYS),
371415
targets_expiration_duration: Duration::days(DEFAULT_TARGETS_EXPIRATION_DAYS),
372416
snapshot_expiration_duration: Duration::days(DEFAULT_SNAPSHOT_EXPIRATION_DAYS),
@@ -455,6 +499,7 @@ where
455499
trusted_targets_keys: vec![],
456500
trusted_snapshot_keys: vec![],
457501
trusted_timestamp_keys: vec![],
502+
time_version: None,
458503
root_expiration_duration: Duration::days(DEFAULT_ROOT_EXPIRATION_DAYS),
459504
targets_expiration_duration: Duration::days(DEFAULT_TARGETS_EXPIRATION_DAYS),
460505
snapshot_expiration_duration: Duration::days(DEFAULT_SNAPSHOT_EXPIRATION_DAYS),
@@ -471,6 +516,23 @@ where
471516
/// Default is the current wall clock time in UTC.
472517
pub fn current_time(mut self, current_time: DateTime<Utc>) -> Self {
473518
self.ctx.current_time = current_time;
519+
520+
// Update our time version if enabled.
521+
if self.ctx.time_version.is_some() {
522+
self.ctx.update_time_version();
523+
}
524+
525+
self
526+
}
527+
528+
/// Create Non-root metadata based off the current UTC timestamp, instead of a monotonic
529+
/// increment.
530+
pub fn time_versioning(mut self, time_versioning: bool) -> Self {
531+
if time_versioning {
532+
self.ctx.update_time_version();
533+
} else {
534+
self.ctx.time_version = None;
535+
}
474536
self
475537
}
476538

@@ -912,9 +974,9 @@ where
912974
let mut delegations_builder = DelegationsBuilder::new();
913975

914976
if let Some(trusted_targets) = self.ctx.db.and_then(|db| db.trusted_targets()) {
915-
let next_version = trusted_targets.version().checked_add(1).ok_or_else(|| {
916-
Error::MetadataVersionMustBeSmallerThanMaxU32(MetadataPath::targets())
917-
})?;
977+
let next_version = self
978+
.ctx
979+
.non_root_next_version(trusted_targets.version(), MetadataPath::targets)?;
918980

919981
targets_builder = targets_builder.version(next_version);
920982

@@ -933,6 +995,8 @@ where
933995
delegations_builder = delegations_builder.role(role.clone());
934996
}
935997
}
998+
} else {
999+
targets_builder = targets_builder.version(self.ctx.non_root_initial_version());
9361000
}
9371001

9381002
// Overwrite any of the old targets with the new ones.
@@ -1092,9 +1156,9 @@ where
10921156
.expires(self.ctx.current_time + self.ctx.snapshot_expiration_duration);
10931157

10941158
if let Some(trusted_snapshot) = self.ctx.db.and_then(|db| db.trusted_snapshot()) {
1095-
let next_version = trusted_snapshot.version().checked_add(1).ok_or_else(|| {
1096-
Error::MetadataVersionMustBeSmallerThanMaxU32(MetadataPath::snapshot())
1097-
})?;
1159+
let next_version = self
1160+
.ctx
1161+
.non_root_next_version(trusted_snapshot.version(), MetadataPath::snapshot)?;
10981162

10991163
snapshot_builder = snapshot_builder.version(next_version);
11001164

@@ -1105,6 +1169,8 @@ where
11051169
.insert_metadata_description(path.clone(), description.clone());
11061170
}
11071171
}
1172+
} else {
1173+
snapshot_builder = snapshot_builder.version(self.ctx.non_root_initial_version());
11081174
}
11091175

11101176
// Overwrite the targets entry if specified.
@@ -1246,14 +1312,13 @@ where
12461312
{
12471313
let next_version = if let Some(db) = self.ctx.db {
12481314
if let Some(trusted_timestamp) = db.trusted_timestamp() {
1249-
trusted_timestamp.version().checked_add(1).ok_or_else(|| {
1250-
Error::MetadataVersionMustBeSmallerThanMaxU32(MetadataPath::timestamp())
1251-
})?
1315+
self.ctx
1316+
.non_root_next_version(trusted_timestamp.version(), MetadataPath::timestamp)?
12521317
} else {
1253-
1
1318+
self.ctx.non_root_initial_version()
12541319
}
12551320
} else {
1256-
1
1321+
self.ctx.non_root_initial_version()
12571322
};
12581323

12591324
let description = if let Some(description) = self.state.snapshot_description()? {
@@ -2975,4 +3040,134 @@ mod tests {
29753040
assert_eq!(db.trusted_timestamp().unwrap().version(), 2);
29763041
})
29773042
}
3043+
3044+
#[test]
3045+
fn test_time_versioning() {
3046+
block_on(async move {
3047+
let mut repo = EphemeralRepository::<Json>::new();
3048+
3049+
let current_time = Utc.timestamp(5, 0);
3050+
let metadata = RepoBuilder::create(&mut repo)
3051+
.current_time(current_time)
3052+
.time_versioning(true)
3053+
.trusted_root_keys(&[&KEYS[0]])
3054+
.trusted_targets_keys(&[&KEYS[0]])
3055+
.trusted_snapshot_keys(&[&KEYS[0]])
3056+
.trusted_timestamp_keys(&[&KEYS[0]])
3057+
.commit()
3058+
.await
3059+
.unwrap();
3060+
3061+
let mut db =
3062+
Database::from_trusted_metadata_with_start_time(&metadata, &current_time).unwrap();
3063+
3064+
// The initial version should be the current time.
3065+
assert_eq!(db.trusted_root().version(), 1);
3066+
assert_eq!(db.trusted_targets().map(|m| m.version()), Some(5));
3067+
assert_eq!(db.trusted_snapshot().map(|m| m.version()), Some(5));
3068+
assert_eq!(db.trusted_timestamp().map(|m| m.version()), Some(5));
3069+
3070+
// Generating metadata for the same timestamp should advance it by 1.
3071+
let metadata = RepoBuilder::from_database(&mut repo, &db)
3072+
.current_time(current_time)
3073+
.time_versioning(true)
3074+
.trusted_root_keys(&[&KEYS[0]])
3075+
.trusted_targets_keys(&[&KEYS[0]])
3076+
.trusted_snapshot_keys(&[&KEYS[0]])
3077+
.trusted_timestamp_keys(&[&KEYS[0]])
3078+
.stage_root()
3079+
.unwrap()
3080+
.stage_targets()
3081+
.unwrap()
3082+
.commit()
3083+
.await
3084+
.unwrap();
3085+
3086+
db.update_metadata_with_start_time(&metadata, &current_time)
3087+
.unwrap();
3088+
3089+
assert_eq!(db.trusted_root().version(), 2);
3090+
assert_eq!(db.trusted_targets().map(|m| m.version()), Some(6));
3091+
assert_eq!(db.trusted_snapshot().map(|m| m.version()), Some(6));
3092+
assert_eq!(db.trusted_timestamp().map(|m| m.version()), Some(6));
3093+
3094+
// Generating metadata for a new timestamp should advance the versions to that amount.
3095+
let current_time = Utc.timestamp(10, 0);
3096+
let metadata = RepoBuilder::from_database(&mut repo, &db)
3097+
.current_time(current_time)
3098+
.time_versioning(true)
3099+
.trusted_root_keys(&[&KEYS[0]])
3100+
.trusted_targets_keys(&[&KEYS[0]])
3101+
.trusted_snapshot_keys(&[&KEYS[0]])
3102+
.trusted_timestamp_keys(&[&KEYS[0]])
3103+
.stage_root()
3104+
.unwrap()
3105+
.stage_targets()
3106+
.unwrap()
3107+
.commit()
3108+
.await
3109+
.unwrap();
3110+
3111+
db.update_metadata_with_start_time(&metadata, &current_time)
3112+
.unwrap();
3113+
3114+
assert_eq!(db.trusted_root().version(), 3);
3115+
assert_eq!(db.trusted_targets().map(|m| m.version()), Some(10));
3116+
assert_eq!(db.trusted_snapshot().map(|m| m.version()), Some(10));
3117+
assert_eq!(db.trusted_timestamp().map(|m| m.version()), Some(10));
3118+
})
3119+
}
3120+
3121+
#[test]
3122+
fn test_time_versioning_falls_back_to_monotonic() {
3123+
block_on(async move {
3124+
let mut repo = EphemeralRepository::<Json>::new();
3125+
3126+
// zero timestamp should initialize to 1.
3127+
let current_time = Utc.timestamp(0, 0);
3128+
let metadata = RepoBuilder::create(&mut repo)
3129+
.current_time(current_time)
3130+
.time_versioning(true)
3131+
.trusted_root_keys(&[&KEYS[0]])
3132+
.trusted_targets_keys(&[&KEYS[0]])
3133+
.trusted_snapshot_keys(&[&KEYS[0]])
3134+
.trusted_timestamp_keys(&[&KEYS[0]])
3135+
.commit()
3136+
.await
3137+
.unwrap();
3138+
3139+
let mut db =
3140+
Database::from_trusted_metadata_with_start_time(&metadata, &current_time).unwrap();
3141+
3142+
assert_eq!(db.trusted_root().version(), 1);
3143+
assert_eq!(db.trusted_targets().map(|m| m.version()), Some(1));
3144+
assert_eq!(db.trusted_snapshot().map(|m| m.version()), Some(1));
3145+
assert_eq!(db.trusted_timestamp().map(|m| m.version()), Some(1));
3146+
3147+
// A sub-second timestamp should advance the version by 1.
3148+
let current_time = Utc.timestamp(0, 3);
3149+
let metadata = RepoBuilder::from_database(&mut repo, &db)
3150+
.current_time(current_time)
3151+
.time_versioning(true)
3152+
.trusted_root_keys(&[&KEYS[0]])
3153+
.trusted_targets_keys(&[&KEYS[0]])
3154+
.trusted_snapshot_keys(&[&KEYS[0]])
3155+
.trusted_timestamp_keys(&[&KEYS[0]])
3156+
.stage_root()
3157+
.unwrap()
3158+
.stage_targets()
3159+
.unwrap()
3160+
.commit()
3161+
.await
3162+
.unwrap();
3163+
3164+
db.update_metadata_with_start_time(&metadata, &current_time)
3165+
.unwrap();
3166+
3167+
assert_eq!(db.trusted_root().version(), 2);
3168+
assert_eq!(db.trusted_targets().map(|m| m.version()), Some(2));
3169+
assert_eq!(db.trusted_snapshot().map(|m| m.version()), Some(2));
3170+
assert_eq!(db.trusted_timestamp().map(|m| m.version()), Some(2));
3171+
})
3172+
}
29783173
}

0 commit comments

Comments
 (0)