Merge pull request #157 from erickt/save2
Clients should locally save metadata
diff --git a/src/client.rs b/src/client.rs
index 95ca100..55fc68f 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -43,18 +43,18 @@
//! local,
//! remote,
//! ).unwrap();
-//! let _ = client.update_local().unwrap();
-//! let _ = client.update_remote().unwrap();
+//! let _ = client.update().unwrap();
//! }
//! ```
use std::io::{Read, Write};
+use chrono::offset::Utc;
use crypto::{self, KeyId};
use error::Error;
use interchange::DataInterchange;
use metadata::{
- Metadata, MetadataPath, MetadataVersion, Role, RootMetadata, SnapshotMetadata,
+ Metadata, MetadataPath, MetadataVersion, Role, RootMetadata, SignedMetadata, SnapshotMetadata,
TargetDescription, TargetPath, TargetsMetadata, VirtualTargetPath,
};
use repository::Repository;
@@ -166,22 +166,31 @@
I: IntoIterator<Item = &'a KeyId>,
T: PathTranslator,
{
+ let root_path = MetadataPath::from_role(&Role::Root);
+
let root = local
.fetch_metadata(
- &MetadataPath::from_role(&Role::Root),
+ &root_path,
&MetadataVersion::Number(1),
&config.max_root_size,
config.min_bytes_per_second,
None,
)
- .or_else(|_| {
- remote.fetch_metadata(
- &MetadataPath::from_role(&Role::Root),
+ .or_else(|_| -> Result<SignedMetadata<_, RootMetadata>> {
+ // FIXME: should we be fetching the latest version instead of version 1?
+ let root = remote.fetch_metadata(
+ &root_path,
&MetadataVersion::Number(1),
&config.max_root_size,
config.min_bytes_per_second,
None,
- )
+ )?;
+
+ local.store_metadata(&root_path, &MetadataVersion::Number(1), &root)?;
+
+ // FIXME: should we also the root as `MetadataVersion::None`?
+
+ Ok(root)
})?;
let tuf = Tuf::from_root_pinned(root, trusted_root_keys)?;
@@ -194,162 +203,175 @@
})
}
- /// Update TUF metadata from the local repository.
- ///
- /// Returns `true` if an update occurred and `false` otherwise.
- pub fn update_local(&mut self) -> Result<bool> {
- let r = Self::update_root(&mut self.tuf, &mut self.local, &self.config)?;
- let ts = match Self::update_timestamp(&mut self.tuf, &mut self.local, &self.config) {
- Ok(b) => b,
- Err(e) => {
- warn!(
- "Error updating timestamp metadata from local sources: {:?}",
- e
- );
- false
- }
- };
- let sn = match Self::update_snapshot(&mut self.tuf, &mut self.local, &self.config) {
- Ok(b) => b,
- Err(e) => {
- warn!(
- "Error updating snapshot metadata from local sources: {:?}",
- e
- );
- false
- }
- };
- let ta = match Self::update_targets(&mut self.tuf, &mut self.local, &self.config) {
- Ok(b) => b,
- Err(e) => {
- warn!(
- "Error updating targets metadata from local sources: {:?}",
- e
- );
- false
- }
- };
-
- Ok(r || ts || sn || ta)
- }
-
/// Update TUF metadata from the remote repository.
///
/// Returns `true` if an update occurred and `false` otherwise.
- pub fn update_remote(&mut self) -> Result<bool> {
- let r = Self::update_root(&mut self.tuf, &mut self.remote, &self.config)?;
- let ts = Self::update_timestamp(&mut self.tuf, &mut self.remote, &self.config)?;
- let sn = Self::update_snapshot(&mut self.tuf, &mut self.remote, &self.config)?;
- let ta = Self::update_targets(&mut self.tuf, &mut self.remote, &self.config)?;
+ pub fn update(&mut self) -> Result<bool> {
+ let r = self.update_root()?;
+ let ts = self.update_timestamp()?;
+ let sn = self.update_snapshot()?;
+ let ta = self.update_targets()?;
Ok(r || ts || sn || ta)
}
- /// Returns `true` if an update occurred and `false` otherwise.
- fn update_root<V, U>(tuf: &mut Tuf<D>, repo: &mut V, config: &Config<U>) -> Result<bool>
+ /// Store the metadata in the local repository. This is juts a local cache, so we ignore if it
+ /// experiences any errors.
+ fn store_metadata<M>(
+ &mut self,
+ path: &MetadataPath,
+ version: &MetadataVersion,
+ metadata: &SignedMetadata<D, M>,
+ )
where
- V: Repository<D>,
- U: PathTranslator,
+ M: Metadata
{
- let latest_root = repo.fetch_metadata::<RootMetadata>(
- &MetadataPath::from_role(&Role::Root),
+ match self.local.store_metadata(path, version, metadata) {
+ Ok(()) => {}
+ Err(err) => {
+ warn!(
+ "failed to store {} metadata version {:?} to {}: {}",
+ M::ROLE.name(),
+ version,
+ path.to_string(),
+ err,
+ );
+ }
+ }
+ }
+
+ /// Returns `true` if an update occurred and `false` otherwise.
+ fn update_root(&mut self) -> Result<bool> {
+ let root_path = MetadataPath::from_role(&Role::Root);
+
+ let latest_root = self.remote.fetch_metadata(
+ &root_path,
&MetadataVersion::None,
- &config.max_root_size,
- config.min_bytes_per_second,
+ &self.config.max_root_size,
+ self.config.min_bytes_per_second,
None,
)?;
- let latest_version = latest_root.as_ref().version();
+ let latest_version = latest_root.version();
- if latest_version < tuf.root().version() {
+ if latest_version < self.tuf.root().version() {
return Err(Error::VerificationFailure(format!(
"Latest root version is lower than current root version: {} < {}",
latest_version,
- tuf.root().version()
+ self.tuf.root().version()
)));
- } else if latest_version == tuf.root().version() {
+ } else if latest_version == self.tuf.root().version() {
return Ok(false);
}
let err_msg = "TUF claimed no update occurred when one should have. \
This is a programming error. Please report this as a bug.";
- for i in (tuf.root().version() + 1)..latest_version {
- let signed = repo.fetch_metadata(
- &MetadataPath::from_role(&Role::Root),
- &MetadataVersion::Number(i),
- &config.max_root_size,
- config.min_bytes_per_second,
+ for i in (self.tuf.root().version() + 1)..latest_version {
+ let version = MetadataVersion::Number(i);
+
+ let signed_root = self.remote.fetch_metadata(
+ &root_path,
+ &version,
+ &self.config.max_root_size,
+ self.config.min_bytes_per_second,
None,
)?;
- if !tuf.update_root(signed)? {
+
+ if !self.tuf.update_root(signed_root.clone())? {
error!("{}", err_msg);
return Err(Error::Programming(err_msg.into()));
}
+
+ self.store_metadata(&root_path, &version, &signed_root);
}
- if !tuf.update_root(latest_root)? {
+ if !self.tuf.update_root(latest_root.clone())? {
error!("{}", err_msg);
return Err(Error::Programming(err_msg.into()));
}
+
+ self.store_metadata(&root_path, &MetadataVersion::Number(latest_version), &latest_root);
+ self.store_metadata(&root_path, &MetadataVersion::None, &latest_root);
+
+ if self.tuf.root().expires() <= &Utc::now() {
+ error!("Root metadata expired, potential freeze attack");
+ return Err(Error::ExpiredMetadata(Role::Root));
+ }
+
Ok(true)
}
/// Returns `true` if an update occurred and `false` otherwise.
- fn update_timestamp<V, U>(tuf: &mut Tuf<D>, repo: &mut V, config: &Config<U>) -> Result<bool>
- where
- V: Repository<D>,
- U: PathTranslator,
- {
- let ts = repo.fetch_metadata(
- &MetadataPath::from_role(&Role::Timestamp),
+ fn update_timestamp(&mut self) -> Result<bool> {
+ let timestamp_path = MetadataPath::from_role(&Role::Timestamp);
+
+ let signed_timestamp = self.remote.fetch_metadata(
+ ×tamp_path,
&MetadataVersion::None,
- &config.max_timestamp_size,
- config.min_bytes_per_second,
+ &self.config.max_timestamp_size,
+ self.config.min_bytes_per_second,
None,
)?;
- tuf.update_timestamp(ts)
+
+ if self.tuf.update_timestamp(signed_timestamp.clone())? {
+ let latest_version = signed_timestamp.version();
+
+ self.store_metadata(
+ ×tamp_path,
+ &MetadataVersion::Number(latest_version),
+ &signed_timestamp,
+ );
+
+ Ok(true)
+ } else {
+ Ok(false)
+ }
}
/// Returns `true` if an update occurred and `false` otherwise.
- fn update_snapshot<V, U>(tuf: &mut Tuf<D>, repo: &mut V, config: &Config<U>) -> Result<bool>
- where
- V: Repository<D>,
- U: PathTranslator,
- {
- let snapshot_description = match tuf.timestamp() {
+ fn update_snapshot(&mut self) -> Result<bool> {
+ // 5.3.1 Check against timestamp metadata. The hashes and version number listed in the
+ // timestamp metadata. If hashes and version do not match, discard the new snapshot
+ // metadata, abort the update cycle, and report the failure.
+ let snapshot_description = match self.tuf.timestamp() {
Some(ts) => Ok(ts.snapshot()),
None => Err(Error::MissingMetadata(Role::Timestamp)),
}?.clone();
- if snapshot_description.version() <= tuf.snapshot().map(|s| s.version()).unwrap_or(0) {
+ if snapshot_description.version() <= self.tuf.snapshot().map(|s| s.version()).unwrap_or(0) {
return Ok(false);
}
let (alg, value) = crypto::hash_preference(snapshot_description.hashes())?;
- let version = if tuf.root().consistent_snapshot() {
+ let version = if self.tuf.root().consistent_snapshot() {
MetadataVersion::Number(snapshot_description.version())
} else {
MetadataVersion::None
};
- let snap = repo.fetch_metadata(
- &MetadataPath::from_role(&Role::Snapshot),
+ let snapshot_path = MetadataPath::from_role(&Role::Snapshot);
+
+ let signed_snapshot = self.remote.fetch_metadata(
+ &snapshot_path,
&version,
&Some(snapshot_description.size()),
- config.min_bytes_per_second,
+ self.config.min_bytes_per_second,
Some((alg, value.clone())),
)?;
- tuf.update_snapshot(snap)
+
+ if self.tuf.update_snapshot(signed_snapshot.clone())? {
+ self.store_metadata(&snapshot_path, &version, &signed_snapshot);
+
+ Ok(true)
+ } else {
+ Ok(false)
+ }
}
/// Returns `true` if an update occurred and `false` otherwise.
- fn update_targets<V, U>(tuf: &mut Tuf<D>, repo: &mut V, config: &Config<U>) -> Result<bool>
- where
- V: Repository<D>,
- U: PathTranslator,
- {
- let targets_description = match tuf.snapshot() {
+ fn update_targets(&mut self) -> Result<bool> {
+ let targets_description = match self.tuf.snapshot() {
Some(sn) => match sn.meta().get(&MetadataPath::from_role(&Role::Targets)) {
Some(d) => Ok(d),
None => Err(Error::VerificationFailure(
@@ -361,26 +383,35 @@
None => Err(Error::MissingMetadata(Role::Snapshot)),
}?.clone();
- if targets_description.version() <= tuf.targets().map(|t| t.version()).unwrap_or(0) {
+ if targets_description.version() <= self.tuf.targets().map(|t| t.version()).unwrap_or(0) {
return Ok(false);
}
let (alg, value) = crypto::hash_preference(targets_description.hashes())?;
- let version = if tuf.root().consistent_snapshot() {
+ let version = if self.tuf.root().consistent_snapshot() {
MetadataVersion::Hash(value.clone())
} else {
MetadataVersion::None
};
- let targets = repo.fetch_metadata(
- &MetadataPath::from_role(&Role::Targets),
+ let targets_path = MetadataPath::from_role(&Role::Targets);
+
+ let signed_targets = self.remote.fetch_metadata(
+ &targets_path,
&version,
&Some(targets_description.size()),
- config.min_bytes_per_second,
+ self.config.min_bytes_per_second,
Some((alg, value.clone())),
)?;
- tuf.update_targets(targets)
+
+ if self.tuf.update_targets(signed_targets.clone())? {
+ self.store_metadata(&targets_path, &version, &signed_targets);
+
+ Ok(true)
+ } else {
+ Ok(false)
+ }
}
/// Fetch a target from the remote repo and write it to the local repo.
@@ -743,10 +774,15 @@
#[cfg(test)]
mod test {
use super::*;
- use crypto::{PrivateKey, SignatureScheme};
+ use chrono::prelude::*;
+ use crypto::{HashAlgorithm, PrivateKey, SignatureScheme};
use interchange::Json;
- use metadata::{MetadataPath, MetadataVersion, RootMetadataBuilder};
+ use metadata::{
+ MetadataPath, MetadataVersion, RootMetadataBuilder, SnapshotMetadataBuilder,
+ TargetsMetadataBuilder, TimestampMetadataBuilder,
+ };
use repository::EphemeralRepository;
+ use std::u32;
lazy_static! {
static ref KEYS: Vec<PrivateKey> = {
@@ -767,8 +803,11 @@
#[test]
fn root_chain_update() {
let repo = EphemeralRepository::new();
- let root = RootMetadataBuilder::new()
+
+ //// First, create the root metadata.
+ let root1 = RootMetadataBuilder::new()
.version(1)
+ .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0))
.root_key(KEYS[0].public().clone())
.snapshot_key(KEYS[0].public().clone())
.targets_key(KEYS[0].public().clone())
@@ -776,14 +815,9 @@
.signed::<Json>(&KEYS[0])
.unwrap();
- repo.store_metadata(
- &MetadataPath::from_role(&Role::Root),
- &MetadataVersion::Number(1),
- &root,
- ).unwrap();
-
- let mut root = RootMetadataBuilder::new()
+ let mut root2 = RootMetadataBuilder::new()
.version(2)
+ .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0))
.root_key(KEYS[1].public().clone())
.snapshot_key(KEYS[1].public().clone())
.targets_key(KEYS[1].public().clone())
@@ -791,16 +825,14 @@
.signed::<Json>(&KEYS[1])
.unwrap();
- root.add_signature(&KEYS[0]).unwrap();
+ root2.add_signature(&KEYS[0]).unwrap();
- repo.store_metadata(
- &MetadataPath::from_role(&Role::Root),
- &MetadataVersion::Number(2),
- &root,
- ).unwrap();
+ // Make sure the version 2 is signed by version 1's keys.
+ root2.add_signature(&KEYS[0]).unwrap();
- let mut root = RootMetadataBuilder::new()
+ let mut root3 = RootMetadataBuilder::new()
.version(3)
+ .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0))
.root_key(KEYS[2].public().clone())
.snapshot_key(KEYS[2].public().clone())
.targets_key(KEYS[2].public().clone())
@@ -808,25 +840,168 @@
.signed::<Json>(&KEYS[2])
.unwrap();
- root.add_signature(&KEYS[1]).unwrap();
+ // Make sure the version 3 is signed by version 2's keys.
+ root3.add_signature(&KEYS[1]).unwrap();
+
+ let mut targets = TargetsMetadataBuilder::new()
+ .signed::<Json>(&KEYS[0])
+ .unwrap();
+
+ targets.add_signature(&KEYS[1]).unwrap();
+ targets.add_signature(&KEYS[2]).unwrap();
+
+ let mut snapshot = SnapshotMetadataBuilder::new()
+ .insert_metadata(&targets, &[HashAlgorithm::Sha256])
+ .unwrap()
+ .signed::<Json>(&KEYS[0])
+ .unwrap();
+
+ snapshot.add_signature(&KEYS[1]).unwrap();
+ snapshot.add_signature(&KEYS[2]).unwrap();
+
+ let mut timestamp = TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256])
+ .unwrap()
+ .signed::<Json>(&KEYS[0])
+ .unwrap();
+
+ timestamp.add_signature(&KEYS[1]).unwrap();
+ timestamp.add_signature(&KEYS[2]).unwrap();
+
+ ////
+ // Now register the metadata.
repo.store_metadata(
&MetadataPath::from_role(&Role::Root),
- &MetadataVersion::Number(3),
- &root,
+ &MetadataVersion::Number(1),
+ &root1,
).unwrap();
+
repo.store_metadata(
&MetadataPath::from_role(&Role::Root),
&MetadataVersion::None,
- &root,
+ &root1,
).unwrap();
- let mut client = Client::new(
- Config::build().finish().unwrap(),
- repo,
- EphemeralRepository::new(),
+ repo.store_metadata(
+ &MetadataPath::from_role(&Role::Targets),
+ &MetadataVersion::Number(1),
+ &targets,
).unwrap();
- assert_eq!(client.update_local(), Ok(true));
+
+ repo.store_metadata(
+ &MetadataPath::from_role(&Role::Targets),
+ &MetadataVersion::None,
+ &targets,
+ ).unwrap();
+
+ repo.store_metadata(
+ &MetadataPath::from_role(&Role::Snapshot),
+ &MetadataVersion::Number(1),
+ &snapshot,
+ ).unwrap();
+
+ repo.store_metadata(
+ &MetadataPath::from_role(&Role::Snapshot),
+ &MetadataVersion::None,
+ &snapshot,
+ ).unwrap();
+
+ repo.store_metadata(
+ &MetadataPath::from_role(&Role::Timestamp),
+ &MetadataVersion::Number(1),
+ ×tamp,
+ ).unwrap();
+
+ repo.store_metadata(
+ &MetadataPath::from_role(&Role::Timestamp),
+ &MetadataVersion::None,
+ ×tamp,
+ ).unwrap();
+
+ ////
+ // Now, make sure that the local metadata got version 1.
+
+ let mut client = Client::with_root_pinned(
+ vec![KEYS[0].public().key_id()],
+ Config::build().finish().unwrap(),
+ EphemeralRepository::new(),
+ repo,
+ ).unwrap();
+
+ assert_eq!(client.update(), Ok(true));
+ assert_eq!(client.tuf.root().version(), 1);
+
+ assert_eq!(
+ root1,
+ client
+ .local
+ .fetch_metadata::<RootMetadata>(
+ &MetadataPath::from_role(&Role::Root),
+ &MetadataVersion::Number(1),
+ &None,
+ u32::MAX,
+ None
+ )
+ .unwrap(),
+ );
+
+ ////
+ // Now bump the root to version 3
+
+ client
+ .remote
+ .store_metadata(
+ &MetadataPath::from_role(&Role::Root),
+ &MetadataVersion::Number(2),
+ &root2,
+ )
+ .unwrap();
+
+ client
+ .remote
+ .store_metadata(
+ &MetadataPath::from_role(&Role::Root),
+ &MetadataVersion::None,
+ &root2,
+ )
+ .unwrap();
+
+ client
+ .remote
+ .store_metadata(
+ &MetadataPath::from_role(&Role::Root),
+ &MetadataVersion::Number(3),
+ &root3,
+ )
+ .unwrap();
+
+ client
+ .remote
+ .store_metadata(
+ &MetadataPath::from_role(&Role::Root),
+ &MetadataVersion::None,
+ &root3,
+ )
+ .unwrap();
+
+ ////
+ // Finally, check that the update brings us to version 3.
+
+ assert_eq!(client.update(), Ok(true));
assert_eq!(client.tuf.root().version(), 3);
+
+ assert_eq!(
+ root3,
+ client
+ .local
+ .fetch_metadata::<RootMetadata>(
+ &MetadataPath::from_role(&Role::Root),
+ &MetadataVersion::Number(3),
+ &None,
+ u32::MAX,
+ None
+ )
+ .unwrap(),
+ );
}
}
diff --git a/tests/simple_example.rs b/tests/simple_example.rs
index 5416c5c..ff7707c 100644
--- a/tests/simple_example.rs
+++ b/tests/simple_example.rs
@@ -59,11 +59,7 @@
{
let local = EphemeralRepository::<Json>::new();
let mut client = Client::with_root_pinned(root_key_ids, config, local, remote)?;
- match client.update_local() {
- Ok(_) => (),
- Err(e) => println!("{:?}", e),
- }
- let _ = client.update_remote()?;
+ let _ = client.update()?;
client.fetch_target(&TargetPath::new("foo-bar".into())?)
}