snapshot metadata
diff --git a/src/client.rs b/src/client.rs
index b3d328e..9946054 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -3,9 +3,10 @@
use chrono::offset::Utc;
use Result;
+use crypto;
use error::Error;
use interchange::DataInterchange;
-use metadata::{MetadataVersion, RootMetadata, Role};
+use metadata::{MetadataVersion, RootMetadata, Role, MetadataPath};
use repository::Repository;
use tuf::Tuf;
@@ -45,8 +46,8 @@
// TODO this might need to be split into `update_local` and `update_remote` to be useful to
// implementers.
pub fn update(&mut self) -> Result<bool> {
- if self.update_root()? {
- self.update_timestamp()
+ if self.update_root()? && self.update_timestamp()? {
+ self.update_snapshot()
} else {
Ok(false)
}
@@ -78,6 +79,7 @@
&Role::Root,
&MetadataVersion::None,
max_root_size,
+ None,
)?;
let latest_version = D::deserialize::<RootMetadata>(latest_root.unverified_signed())?
.version();
@@ -100,6 +102,7 @@
&Role::Root,
&MetadataVersion::Number(i),
max_root_size,
+ None,
)?;
if !tuf.update_root(signed)? {
error!("{}", err_msg);
@@ -120,6 +123,7 @@
&Role::Timestamp,
&MetadataVersion::None,
&self.config.max_timestamp_size,
+ None,
)?;
self.tuf.update_timestamp(ts)?;
@@ -127,9 +131,49 @@
&Role::Timestamp,
&MetadataVersion::None,
&self.config.max_timestamp_size,
+ None,
)?;
self.tuf.update_timestamp(ts)
}
+
+ /// Returns `true` if an update occurred and `false` otherwise.
+ fn update_snapshot(&mut self) -> Result<bool> {
+ let snapshot_description = match self.tuf.timestamp() {
+ Some(ts) => {
+ match ts.meta().get(&MetadataPath::from_role(&Role::Timestamp)) {
+ Some(d) => Ok(d),
+ None => Err(Error::VerificationFailure(
+ "Timestamp metadata did not contain a description of the \
+ current snapshot metadata"
+ .into(),
+ )),
+ }
+ }
+ None => Err(Error::MissingMetadata(Role::Timestamp)),
+ }?
+ .clone();
+
+ let hashes = match snapshot_description.hashes() {
+ Some(hashes) => Some(crypto::hash_preference(hashes)?),
+ None => None,
+ };
+
+ let snap = self.local.fetch_metadata(
+ &Role::Snapshot,
+ &MetadataVersion::None,
+ &snapshot_description.length(),
+ hashes,
+ )?;
+ self.tuf.update_snapshot(snap)?;
+
+ let snap = self.remote.fetch_metadata(
+ &Role::Snapshot,
+ &MetadataVersion::None,
+ &snapshot_description.length(),
+ hashes,
+ )?;
+ self.tuf.update_snapshot(snap)
+ }
}
/// Configuration for a TUF `Client`.
diff --git a/src/crypto.rs b/src/crypto.rs
index 64e7de7..0e4b097 100644
--- a/src/crypto.rs
+++ b/src/crypto.rs
@@ -6,6 +6,7 @@
use ring::signature::{ED25519, RSA_PSS_2048_8192_SHA256, RSA_PSS_2048_8192_SHA512};
use serde::de::{Deserialize, Deserializer, Error as DeserializeError};
use serde::ser::{Serialize, Serializer, SerializeTupleStruct, Error as SerializeError};
+use std::collections::HashMap;
use std::fmt::{self, Debug};
use std::str::FromStr;
use untrusted::Input;
@@ -15,6 +16,22 @@
use rsa;
use shims;
+static HASH_ALG_PREFS: &'static [HashAlgorithm] = &[HashAlgorithm::Sha512, HashAlgorithm::Sha256];
+
+/// Given a map of hash algorithms and their values, get the prefered algorithm and the hash
+/// calculated by it. Returns an `Err` if there is no match.
+pub fn hash_preference<'a>(
+ hashes: &'a HashMap<HashAlgorithm, HashValue>,
+) -> Result<(&'static HashAlgorithm, &'a HashValue)> {
+ for alg in HASH_ALG_PREFS {
+ match hashes.get(alg) {
+ Some(v) => return Ok((alg, v)),
+ None => continue,
+ }
+ }
+ Err(Error::NoSupportedHashAlgorithm)
+}
+
/// Calculate the given key's ID.
///
/// A `KeyId` is calculated as `sha256(public_key_bytes)`. The TUF spec says that it should be
@@ -232,19 +249,17 @@
let pkcs1_value = match format {
KeyFormat::Pkcs1 => {
- let bytes = rsa::from_pkcs1(value.value()).ok_or(
+ let bytes = rsa::from_pkcs1(value.value()).ok_or_else(|| {
Error::IllegalArgument(
- "Key claimed to be PKCS1 but could not be parsed."
- .into(),
- ),
- )?;
+ "Key claimed to be PKCS1 but could not be parsed.".into(),
+ )
+ })?;
PublicKeyValue(bytes)
}
KeyFormat::Spki => {
- let bytes = rsa::from_spki(value.value()).ok_or(Error::IllegalArgument(
- "Key claimed to be SPKI but could not be parsed."
- .into(),
- ))?;
+ let bytes = rsa::from_spki(value.value()).ok_or_else(|| {
+ Error::IllegalArgument("Key claimed to be SPKI but could not be parsed.".into())
+ })?;
PublicKeyValue(bytes)
}
x => {
@@ -385,3 +400,10 @@
/// Wrapper for the value of a hash digest.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HashValue(Vec<u8>);
+
+impl HashValue {
+ /// An immutable reference to the bytes of the hash value.
+ pub fn value(&self) -> &[u8] {
+ &self.0
+ }
+}
diff --git a/src/error.rs b/src/error.rs
index 33c862e..4d4840b 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -29,6 +29,8 @@
Io(String),
/// The metadata was missing, so an operation could not be completed.
MissingMetadata(Role),
+ /// There were no available hash algorithms.
+ NoSupportedHashAlgorithm,
/// The metadata or target was not found.
NotFound,
/// There was an internal `serde` error.
diff --git a/src/metadata.rs b/src/metadata.rs
index 7479a65..04848aa 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -376,6 +376,11 @@
pub struct MetadataPath(String);
impl MetadataPath {
+ /// Create a metadata path from the given role.
+ pub fn from_role(role: &Role) -> Self {
+ MetadataPath(role.to_string())
+ }
+
// TODO convert to/from paths/urls/etc
}
@@ -495,6 +500,77 @@
}
}
+/// Metdata for the snapshot role.
+#[derive(Debug, PartialEq)]
+pub struct SnapshotMetadata {
+ version: u32,
+ expires: DateTime<Utc>,
+ meta: HashMap<MetadataPath, MetadataDescription>,
+}
+
+impl SnapshotMetadata {
+ /// Create new `SnapshotMetadata`.
+ pub fn new(
+ version: u32,
+ expires: DateTime<Utc>,
+ meta: HashMap<MetadataPath, MetadataDescription>,
+ ) -> Result<Self> {
+ if version < 1 {
+ return Err(Error::IllegalArgument(format!(
+ "Metadata version must be greater than zero. Found: {}",
+ version
+ )));
+ }
+
+ Ok(SnapshotMetadata {
+ version: version,
+ expires: expires,
+ meta: meta,
+ })
+ }
+
+ /// The version number.
+ pub fn version(&self) -> u32 {
+ self.version
+ }
+
+ /// An immutable reference to the metadata's expiration `DateTime`.
+ pub fn expires(&self) -> &DateTime<Utc> {
+ &self.expires
+ }
+
+ /// An immutable reference to the metadata paths and descriptions.
+ pub fn meta(&self) -> &HashMap<MetadataPath, MetadataDescription> {
+ &self.meta
+ }
+}
+
+impl Metadata for SnapshotMetadata {
+ fn role() -> Role {
+ Role::Snapshot
+ }
+}
+
+impl Serialize for SnapshotMetadata {
+ fn serialize<S>(&self, ser: S) -> ::std::result::Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ shims::SnapshotMetadata::from(self)
+ .map_err(|e| SerializeError::custom(format!("{:?}", e)))?
+ .serialize(ser)
+ }
+}
+
+impl<'de> Deserialize<'de> for SnapshotMetadata {
+ fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
+ let intermediate: shims::SnapshotMetadata = Deserialize::deserialize(de)?;
+ intermediate.try_into().map_err(|e| {
+ DeserializeError::custom(format!("{:?}", e))
+ })
+ }
+}
+
#[cfg(test)]
mod test {
use json;
diff --git a/src/repository.rs b/src/repository.rs
index a9e13ed..7b8a6a1 100644
--- a/src/repository.rs
+++ b/src/repository.rs
@@ -3,13 +3,15 @@
use hyper::{Url, Client};
use hyper::client::response::Response;
use hyper::header::{Headers, UserAgent};
+use ring::digest::{self, SHA256, SHA512};
use std::collections::HashMap;
use std::fs::{self, File, DirBuilder};
-use std::io::Read;
+use std::io::{Read, Write};
use std::marker::PhantomData;
use std::path::PathBuf;
use Result;
+use crypto::{HashAlgorithm, HashValue};
use error::Error;
use metadata::{SignedMetadata, MetadataVersion, Unverified, Verified, Role, Metadata};
use interchange::DataInterchange;
@@ -39,6 +41,7 @@
role: &Role,
version: &MetadataVersion,
max_size: &Option<usize>,
+ hash_data: Option<(&HashAlgorithm, &HashValue)>,
) -> Result<SignedMetadata<D, M, Unverified>>
where
M: Metadata;
@@ -49,19 +52,75 @@
format!("{}{}{}", version.prefix(), role, D::extension())
}
- /// Read the from given reader, optionally capped at `max_size` bytes.
- fn safe_read<R: Read>(read: &mut R, max_size: &Option<usize>) -> Result<Vec<u8>> {
- match max_size {
- &Some(max_size) => {
- let mut buf = vec![0; max_size];
- read.read_exact(&mut buf)?;
- Ok(buf)
+ /// Read the from given reader, optionally capped at `max_size` bytes, optionally requiring
+ /// hashes to match.
+ fn safe_read<R, W>(
+ read: &mut R,
+ write: &mut W,
+ max_size: Option<i64>,
+ hash_data: Option<(&HashAlgorithm, &HashValue)>,
+ ) -> Result<()>
+ where
+ R: Read,
+ W: Write,
+ {
+ let mut context = match hash_data {
+ Some((&HashAlgorithm::Sha256, _)) => Some(digest::Context::new(&SHA256)),
+ Some((&HashAlgorithm::Sha512, _)) => Some(digest::Context::new(&SHA512)),
+ None => None,
+ };
+
+ let mut buf = [0; 1024];
+ let mut bytes_left = max_size.unwrap_or(::std::i64::MAX);
+
+ loop {
+ match read.read(&mut buf) {
+ Ok(read_bytes) => {
+ if read_bytes == 0 {
+ break;
+ }
+
+ bytes_left -= read_bytes as i64;
+ if bytes_left < 0 {
+ return Err(Error::VerificationFailure(
+ "Read exceeded the maximum allowed bytes.".into(),
+ ));
+ }
+
+ write.write_all(&buf[0..read_bytes])?;
+
+ match context {
+ Some(ref mut c) => c.update(&buf[0..read_bytes]),
+ None => (),
+ };
+ }
+ e @ Err(_) => e.map(|_| ())?,
}
- &None => {
- let mut buf = Vec::new();
- let _ = read.read_to_end(&mut buf)?;
- Ok(buf)
+ }
+
+ let generated_hash = context.map(|c| c.finish());
+
+ match (generated_hash, hash_data) {
+ (Some(generated_hash), Some((_, expected_hash)))
+ if generated_hash.as_ref() != expected_hash.value() => {
+ Err(Error::VerificationFailure(
+ "Generated hash did not match expected hash.".into(),
+ ))
}
+ (Some(_), None) => {
+ let msg = "Hash calculated when no expected hash supplied. \
+ This is a programming error. Please report this as a bug.";
+ error!("{}", msg);
+ Err(Error::Generic(msg.into()))
+ }
+ (None, Some(_)) => {
+ let msg = "No hash calculated when expected hash supplied. \
+ This is a programming error. Please report this as a bug.";
+ error!("{}", msg);
+ Err(Error::Generic(msg.into()))
+ }
+ (Some(_), Some(_)) |
+ (None, None) => Ok(()),
}
}
}
@@ -138,6 +197,7 @@
role: &Role,
version: &MetadataVersion,
max_size: &Option<usize>,
+ hash_data: Option<(&HashAlgorithm, &HashValue)>,
) -> Result<SignedMetadata<D, M, Unverified>>
where
M: Metadata,
@@ -145,8 +205,9 @@
let version_str = Self::version_string(role, version);
let path = self.local_path.join("metadata").join(&version_str);
let mut file = File::open(&path)?;
- let buf = Self::safe_read(&mut file, max_size)?;
- Ok(D::from_reader(&*buf)?)
+ let mut out = Vec::new();
+ Self::safe_read(&mut file, &mut out, max_size.map(|x| x as i64), hash_data)?;
+ Ok(D::from_reader(&*out)?)
}
}
@@ -226,14 +287,16 @@
role: &Role,
version: &MetadataVersion,
max_size: &Option<usize>,
+ hash_data: Option<(&HashAlgorithm, &HashValue)>,
) -> Result<SignedMetadata<D, M, Unverified>>
where
M: Metadata,
{
let version_str = Self::version_string(role, version);
let mut resp = self.get(&version_str)?;
- let buf = Self::safe_read(&mut resp, max_size)?;
- Ok(D::from_reader(&*buf)?)
+ let mut out = Vec::new();
+ Self::safe_read(&mut resp, &mut out, max_size.map(|x| x as i64), hash_data)?;
+ Ok(D::from_reader(&*out)?)
}
}
@@ -297,6 +360,7 @@
role: &Role,
version: &MetadataVersion,
_: &Option<usize>,
+ _: Option<(&HashAlgorithm, &HashValue)>,
) -> Result<SignedMetadata<D, M, Unverified>>
where
M: Metadata,
diff --git a/src/shims.rs b/src/shims.rs
index db461fc..c9b4734 100644
--- a/src/shims.rs
+++ b/src/shims.rs
@@ -66,29 +66,21 @@
}
}
- let root = self.roles.remove(&metadata::Role::Root).ok_or(
- Error::Decode(
- "Missing root role definition"
- .into(),
- ),
+ let root = self.roles.remove(&metadata::Role::Root).ok_or_else(|| {
+ Error::Decode("Missing root role definition".into())
+ })?;
+ let snapshot = self.roles.remove(&metadata::Role::Snapshot).ok_or_else(
+ || {
+ Error::Decode("Missing snapshot role definition".into())
+ },
)?;
- let snapshot = self.roles.remove(&metadata::Role::Snapshot).ok_or(
- Error::Decode(
- "Missing snapshot role definition"
- .into(),
- ),
- )?;
- let targets = self.roles.remove(&metadata::Role::Targets).ok_or(
- Error::Decode(
- "Missing targets role definition"
- .into(),
- ),
- )?;
- let timestamp = self.roles.remove(&metadata::Role::Timestamp).ok_or(
- Error::Decode(
- "Missing timestamp role definition"
- .into(),
- ),
+ let targets = self.roles.remove(&metadata::Role::Targets).ok_or_else(|| {
+ Error::Decode("Missing targets role definition".into())
+ })?;
+ let timestamp = self.roles.remove(&metadata::Role::Timestamp).ok_or_else(
+ || {
+ Error::Decode("Missing timestamp role definition".into())
+ },
)?;
metadata::RootMetadata::new(
@@ -245,3 +237,34 @@
metadata::TimestampMetadata::new(self.version, self.expires, self.meta)
}
}
+
+#[derive(Serialize, Deserialize)]
+pub struct SnapshotMetadata {
+ #[serde(rename = "type")]
+ typ: metadata::Role,
+ version: u32,
+ expires: DateTime<Utc>,
+ meta: HashMap<metadata::MetadataPath, metadata::MetadataDescription>,
+}
+
+impl SnapshotMetadata {
+ pub fn from(metadata: &metadata::SnapshotMetadata) -> Result<Self> {
+ Ok(SnapshotMetadata {
+ typ: metadata::Role::Snapshot,
+ version: metadata.version(),
+ expires: metadata.expires().clone(),
+ meta: metadata.meta().clone(),
+ })
+ }
+
+ pub fn try_into(self) -> Result<metadata::SnapshotMetadata> {
+ if self.typ != metadata::Role::Snapshot {
+ return Err(Error::Decode(format!(
+ "Attempted to decode snapshot metdata labeled as {:?}",
+ self.typ
+ )));
+ }
+
+ metadata::SnapshotMetadata::new(self.version, self.expires, self.meta)
+ }
+}
diff --git a/src/tuf.rs b/src/tuf.rs
index 678fbb9..62a66fe 100644
--- a/src/tuf.rs
+++ b/src/tuf.rs
@@ -7,12 +7,14 @@
use crypto::KeyId;
use error::Error;
use interchange::DataInterchange;
-use metadata::{SignedMetadata, RootMetadata, VerificationStatus, TimestampMetadata, Role};
+use metadata::{SignedMetadata, RootMetadata, VerificationStatus, TimestampMetadata, Role,
+ SnapshotMetadata, MetadataPath};
/// Contains trusted TUF metadata and can be used to verify other metadata and targets.
#[derive(Debug)]
pub struct Tuf<D: DataInterchange> {
root: RootMetadata,
+ snapshot: Option<SnapshotMetadata>,
timestamp: Option<TimestampMetadata>,
_interchange: PhantomData<D>,
}
@@ -49,6 +51,7 @@
)?;
Ok(Tuf {
root: root,
+ snapshot: None,
timestamp: None,
_interchange: PhantomData,
})
@@ -104,6 +107,8 @@
root.keys(),
)?;
+ self.purge_metadata();
+
self.root = root;
Ok(true)
}
@@ -141,4 +146,96 @@
Ok(true)
}
}
+
+ /// Verify and update the snapshot metadata.
+ pub fn update_snapshot<V>(
+ &mut self,
+ signed_snapshot: SignedMetadata<D, SnapshotMetadata, V>,
+ ) -> Result<bool>
+ where
+ V: VerificationStatus,
+ {
+ let root = self.safe_root_ref()?;
+ let timestamp = self.safe_timestamp_ref()?;
+ let snapshot_description = timestamp
+ .meta()
+ .get(&MetadataPath::from_role(&Role::Snapshot))
+ .ok_or_else(|| {
+ Error::VerificationFailure(
+ "Timestamp metadata had no description of the snapshot metadata".into(),
+ )
+ })?;
+
+ let signed_snapshot = signed_snapshot.verify(
+ root.snapshot().threshold(),
+ root.snapshot().key_ids(),
+ root.keys(),
+ )?;
+
+ let current_version = self.snapshot.as_ref().map(|t| t.version()).unwrap_or(0);
+ let snapshot: SnapshotMetadata = D::deserialize(&signed_snapshot.signed())?;
+
+ if snapshot.version() != snapshot_description.version() {
+ return Err(Error::VerificationFailure(format!(
+ "The timestamp metadata reported that the snapshot metadata should be at \
+ version {} but version {} was found instead.",
+ snapshot_description.version(),
+ snapshot.version()
+ )));
+ }
+
+ if snapshot.expires() <= &Utc::now() {
+ return Err(Error::ExpiredMetadata(Role::Snapshot));
+ }
+
+ if snapshot.version() < current_version {
+ Err(Error::VerificationFailure(format!(
+ "Attempted to roll back snapshot metdata at version {} to {}.",
+ current_version,
+ snapshot.version()
+ )))
+ } else if snapshot.version() == current_version {
+ Ok(false)
+ } else {
+ Ok(true)
+ }
+ }
+
+ fn purge_metadata(&mut self) {
+ self.snapshot = None;
+ self.timestamp = None;
+ // TODO include targets
+ // TODO include delegations
+ }
+
+ fn safe_root_ref(&self) -> Result<&RootMetadata> {
+ if self.root.expires() <= &Utc::now() {
+ return Err(Error::ExpiredMetadata(Role::Root));
+ }
+ Ok(&self.root)
+ }
+
+ fn safe_timestamp_ref(&self) -> Result<&TimestampMetadata> {
+ match &self.timestamp {
+ &Some(ref ts) => {
+ if self.root.expires() <= &Utc::now() {
+ return Err(Error::ExpiredMetadata(Role::Root));
+ }
+ Ok(ts)
+ }
+ &None => Err(Error::MissingMetadata(Role::Timestamp)),
+ }
+ }
+
+ fn safe_snapshot_ref(&self) -> Result<&SnapshotMetadata> {
+ match &self.snapshot {
+ &Some(ref ts) => {
+ if self.root.expires() <= &Utc::now() {
+ return Err(Error::ExpiredMetadata(Role::Root));
+ }
+ Ok(ts)
+ }
+ &None => Err(Error::MissingMetadata(Role::Snapshot)),
+ }
+ }
}