diff --git a/tough/src/cache.rs b/tough/src/cache.rs index 69c0c5102..5c9e23f22 100644 --- a/tough/src/cache.rs +++ b/tough/src/cache.rs @@ -1,5 +1,5 @@ use crate::error::{self, Result}; -use crate::fetch::{fetch_max_size, fetch_sha256}; +use crate::fetch::{fetch_max_size, fetch_sha256, fetch_sha512}; use crate::schema::{RoleType, Target}; use crate::transport::IntoVec; use crate::{encode_filename, Prefix, Repository, TargetName}; @@ -257,15 +257,27 @@ impl Repository { &self, target: &Target, name: &TargetName, - ) -> (Vec, String) { - let sha256 = &target.hashes.sha256.clone().into_vec(); + ) -> Result<(Vec, String)> { + let sha256 = target.hashes.sha256.as_ref().map(|d| d.clone().into_vec()); + let sha512 = target.hashes.sha512.as_ref().map(|d| d.clone().into_vec()); + + let digest = if let Some(sha256) = sha256 { + sha256 + } else if let Some(sha512) = sha512 { + sha512 + } else { + return Err(error::NoValidHashSnafu { + name: format!("{:?}", name), + } + .build()); + }; if self.consistent_snapshot { - ( - sha256.clone(), - format!("{}.{}", hex::encode(sha256), name.resolved()), - ) + Ok(( + digest.clone(), + format!("{}.{}", hex::encode(digest), name.resolved()), + )) } else { - (sha256.clone(), name.resolved().to_owned()) + Ok((digest, name.resolved().to_owned())) } } @@ -284,15 +296,28 @@ impl Repository { path: filename, url: self.targets_base_url.clone(), })?; - Ok(fetch_sha256( - self.transport.as_ref(), - url.clone(), - target.length, - "targets.json", - digest, - ) - .await? - .context(error::TransportSnafu { url }) - .boxed()) + if target.hashes.sha256.is_some() { + Ok(fetch_sha256( + self.transport.as_ref(), + url.clone(), + target.length, + "targets.json", + digest, + ) + .await? + .context(error::TransportSnafu { url }) + .boxed()) + } else { + Ok(fetch_sha512( + self.transport.as_ref(), + url.clone(), + target.length, + "targets.json", + digest, + ) + .await? + .context(error::TransportSnafu { url }) + .boxed()) + } } } diff --git a/tough/src/editor/mod.rs b/tough/src/editor/mod.rs index 6ec91b278..e92b322ce 100644 --- a/tough/src/editor/mod.rs +++ b/tough/src/editor/mod.rs @@ -24,7 +24,7 @@ use crate::transport::{IntoVec, Transport}; use crate::{encode_filename, Limits}; use crate::{Repository, TargetName}; use chrono::{DateTime, Utc}; -use ring::digest::{SHA256, SHA256_OUTPUT_LEN}; +use ring::digest::{SHA256, SHA256_OUTPUT_LEN, SHA512, SHA512_OUTPUT_LEN}; use ring::rand::SystemRandom; use serde_json::Value; use snafu::{ensure, OptionExt, ResultExt}; @@ -112,13 +112,17 @@ impl RepositoryEditor { } } - let mut digest = [0; SHA256_OUTPUT_LEN]; - digest.copy_from_slice(ring::digest::digest(&SHA256, &root_buf).as_ref()); + let mut sha256_digest = [0; SHA256_OUTPUT_LEN]; + sha256_digest.copy_from_slice(ring::digest::digest(&SHA256, &root_buf).as_ref()); + + let mut sha512_digest = [0; SHA512_OUTPUT_LEN]; + sha512_digest.copy_from_slice(ring::digest::digest(&SHA512, &root_buf).as_ref()); let signed_root = SignedRole { signed: root, buffer: root_buf, - sha256: digest, + sha256: sha256_digest, + sha512: sha512_digest, length: root_buf_len, }; @@ -708,7 +712,8 @@ impl RepositoryEditor { { Metafile { hashes: Some(Hashes { - sha256: role.sha256.to_vec().into(), + sha256: Some(role.sha256.to_vec().into()), + sha512: Some(role.sha512.to_vec().into()), _extra: HashMap::new(), }), length: Some(role.length), @@ -746,7 +751,8 @@ impl RepositoryEditor { { Metafile { hashes: Some(Hashes { - sha256: role.sha256.to_vec().into(), + sha256: Some(role.sha256.to_vec().into()), + sha512: Some(role.sha512.to_vec().into()), _extra: HashMap::new(), }), length: Some(role.length), diff --git a/tough/src/editor/signed.rs b/tough/src/editor/signed.rs index eea91d056..d37b900dc 100644 --- a/tough/src/editor/signed.rs +++ b/tough/src/editor/signed.rs @@ -16,7 +16,7 @@ use crate::schema::{ use async_trait::async_trait; use futures::TryStreamExt; use olpc_cjson::CanonicalFormatter; -use ring::digest::{digest, SHA256, SHA256_OUTPUT_LEN}; +use ring::digest::{digest, SHA256, SHA256_OUTPUT_LEN, SHA512, SHA512_OUTPUT_LEN}; use ring::rand::SecureRandom; use serde::{Deserialize, Serialize}; use serde_plain::derive_fromstr_from_deserialize; @@ -48,6 +48,7 @@ pub struct SignedRole { pub(crate) signed: Signed, pub(crate) buffer: Vec, pub(crate) sha256: [u8; SHA256_OUTPUT_LEN], + pub(crate) sha512: [u8; SHA512_OUTPUT_LEN], pub(crate) length: u64, } @@ -123,12 +124,17 @@ where let mut sha256 = [0; SHA256_OUTPUT_LEN]; sha256.copy_from_slice(digest(&SHA256, &buffer).as_ref()); + // Calculate SHA-512 + let mut sha512 = [0; SHA512_OUTPUT_LEN]; + sha512.copy_from_slice(digest(&SHA512, &buffer).as_ref()); + // Create the `SignedRole` containing, the `Signed`, serialized // buffer, length and sha256. let signed_role = SignedRole { signed: role, buffer, sha256, + sha512, length, }; @@ -147,8 +153,26 @@ where } /// Provides the sha256 digest of the signed role. - pub fn sha256(&self) -> &[u8] { - &self.sha256 + // pub fn sha256(&self) -> &[u8] { + // &self.sha256 + // } + + /// Provides the sha256 digest of the signed role, if available. + pub fn sha256(&self) -> Option<&[u8]> { + if self.sha256.iter().any(|&byte| byte != 0) { + Some(&self.sha256) + } else { + None + } + } + + /// Provides the sha512 digest of the signed role, if available. + pub fn sha512(&self) -> Option<&[u8]> { + if self.sha512.iter().any(|&byte| byte != 0) { + Some(&self.sha512) + } else { + None + } } /// Provides the length in bytes of the serialized representation of the signed role. @@ -762,18 +786,61 @@ trait TargetsWalker { // should match, or we alert the caller; if target replacement is intended, it should // happen earlier, in RepositoryEditor. ensure!( - target_from_path.hashes.sha256 == repo_target.hashes.sha256, + target_from_path.hashes.sha256 == repo_target.hashes.sha256 + || target_from_path.hashes.sha512 == repo_target.hashes.sha512, error::HashMismatchSnafu { context: "target", - calculated: hex::encode(target_from_path.hashes.sha256), - expected: hex::encode(&repo_target.hashes.sha256), + calculated: format!( + "SHA-256: {}, SHA-512: {}", + hex::encode( + target_from_path + .hashes + .sha256 + .as_ref() + .map(|d| d.as_ref()) + .unwrap_or(&[]) + ), + hex::encode( + target_from_path + .hashes + .sha512 + .as_ref() + .map(|d| d.as_ref()) + .unwrap_or(&[]) + ) + ), + expected: format!( + "SHA-256: {}, SHA-512: {}", + hex::encode( + repo_target + .hashes + .sha256 + .as_ref() + .map(|d| d.as_ref()) + .unwrap_or(&[]) + ), + hex::encode( + repo_target + .hashes + .sha512 + .as_ref() + .map(|d| d.as_ref()) + .unwrap_or(&[]) + ) + ), } ); let dest = if self.consistent_snapshot() { outdir.join(format!( "{}.{}", - hex::encode(&target_from_path.hashes.sha256), + hex::encode( + target_from_path + .hashes + .sha256 + .or(target_from_path.hashes.sha512) + .unwrap() + ), target_name.resolved() )) } else { @@ -798,14 +865,57 @@ trait TargetsWalker { .fetch(url.clone()) .await .with_context(|_| error::TransportSnafu { url: url.clone() })?; - let stream = DigestAdapter::sha256(stream, &repo_target.hashes.sha256, url.clone()); - // The act of reading with the DigestAdapter verifies the checksum, assuming the read - // succeeds. - stream - .try_for_each(|_| ready(Ok(()))) - .await - .context(error::TransportSnafu { url })?; + let sha256_verified = if let Some(sha256) = &repo_target.hashes.sha256 { + let sha256_stream = DigestAdapter::sha256(stream, sha256, url.clone()); + // Verify SHA-256 checksum + sha256_stream.try_for_each(|_| ready(Ok(()))).await.is_ok() + } else { + false + }; + + let sha512_verified = if !sha256_verified { + let stream = FilesystemTransport + .fetch(url.clone()) + .await + .with_context(|_| error::TransportSnafu { url: url.clone() })?; + if let Some(sha512) = &repo_target.hashes.sha512 { + let sha512_stream = DigestAdapter::sha512(stream, sha512, url.clone()); + // Verify SHA-512 checksum + sha512_stream.try_for_each(|_| ready(Ok(()))).await.is_ok() + } else { + false + } + } else { + true + }; + + if !sha256_verified && !sha512_verified { + return error::HashMismatchSnafu { + context: "target", + calculated: format!( + "SHA-256: {}, SHA-512: {}", + hex::encode( + repo_target + .hashes + .sha256 + .as_ref() + .map(|d| d.as_ref()) + .unwrap_or(&[]) + ), + hex::encode( + repo_target + .hashes + .sha512 + .as_ref() + .map(|d| d.as_ref()) + .unwrap_or(&[]) + ) + ), + expected: "None".to_string(), + } + .fail(); + } } let metadata = symlink_metadata(&dest) diff --git a/tough/src/error.rs b/tough/src/error.rs index 403998b27..f49f3cf9d 100644 --- a/tough/src/error.rs +++ b/tough/src/error.rs @@ -141,6 +141,9 @@ pub enum Error { #[snafu(display("Source path for target must be file or symlink - '{}'", path.display()))] InvalidFileType { path: PathBuf, backtrace: Backtrace }, + #[snafu(display("No valid hash was found for target '{:?}'", name))] + NoValidHash { name: String, backtrace: Backtrace }, + #[snafu(display("Encountered an invalid target name: {}", inner))] InvalidTargetName { inner: String, backtrace: Backtrace }, diff --git a/tough/src/fetch.rs b/tough/src/fetch.rs index cdebf736f..a43d601ff 100644 --- a/tough/src/fetch.rs +++ b/tough/src/fetch.rs @@ -32,3 +32,14 @@ pub(crate) async fn fetch_sha256( let stream = fetch_max_size(transport, url.clone(), size, specifier).await?; Ok(DigestAdapter::sha256(stream, sha256, url)) } + +pub(crate) async fn fetch_sha512( + transport: &dyn Transport, + url: Url, + size: u64, + specifier: &'static str, + sha512: &[u8], +) -> Result { + let stream = fetch_max_size(transport, url.clone(), size, specifier).await?; + Ok(DigestAdapter::sha512(stream, sha512, url)) +} diff --git a/tough/src/io.rs b/tough/src/io.rs index f34be54b4..6453a0f20 100644 --- a/tough/src/io.rs +++ b/tough/src/io.rs @@ -4,7 +4,7 @@ use crate::{error, transport::TransportStream, TransportError}; use futures::StreamExt; use futures_core::Stream; -use ring::digest::{Context, SHA256}; +use ring::digest::{Context, SHA256, SHA512}; use std::{convert::TryInto, path::Path, task::Poll}; use tokio::fs; use url::Url; @@ -26,6 +26,15 @@ impl DigestAdapter { } .boxed() } + pub(crate) fn sha512(stream: TransportStream, hash: &[u8], url: Url) -> TransportStream { + Self { + url, + stream, + hash: hash.to_owned(), + digest: Context::new(&SHA512), + } + .boxed() + } } impl Stream for DigestAdapter { diff --git a/tough/src/lib.rs b/tough/src/lib.rs index 952642519..f7018d541 100644 --- a/tough/src/lib.rs +++ b/tough/src/lib.rs @@ -47,7 +47,7 @@ mod urlpath; use crate::datastore::Datastore; use crate::error::Result; -use crate::fetch::{fetch_max_size, fetch_sha256}; +use crate::fetch::{fetch_max_size, fetch_sha256, fetch_sha512}; /// An HTTP transport that includes retries. #[cfg(feature = "http")] pub use crate::http::{HttpTransport, HttpTransportBuilder}; @@ -479,7 +479,7 @@ impl Repository { // found earlier in step 4. In either case, the client MUST write the file to // non-volatile storage as FILENAME.EXT. Ok(if let Ok(target) = self.targets.signed.find_target(name) { - let (sha256, file) = self.target_digest_and_filename(target, name); + let (sha256, file) = self.target_digest_and_filename(target, name)?; Some(self.fetch_target(target, &sha256, file.as_str()).await?) } else { None @@ -535,8 +535,17 @@ impl Repository { target_name: name.clone(), } })?; - let sha256 = target.hashes.sha256.clone().into_vec(); - format!("{}.{}", hex::encode(sha256), name.resolved()) + let digest = if let Some(sha256) = &target.hashes.sha256 { + sha256.clone().into_vec() + } else if let Some(sha512) = &target.hashes.sha512 { + sha512.clone().into_vec() + } else { + return error::NoValidHashSnafu { + name: format!("{:?}", name), + } + .fail(); + }; + format!("{}.{}", hex::encode(digest), name.resolved()) } Prefix::None => name.resolved().to_owned(), }; @@ -950,14 +959,33 @@ async fn load_snapshot( url: metadata_base_url.clone(), })?; let stream = if let Some(hashes) = &snapshot_meta.hashes { - fetch_sha256( - transport, - url.clone(), - snapshot_meta.length.unwrap_or(max_snapshot_size), - "timestamp.json", - &hashes.sha256, - ) - .await? + if let Some(sha256_hash) = &hashes.sha256 { + fetch_sha256( + transport, + url.clone(), + snapshot_meta.length.unwrap_or(max_snapshot_size), + "timestamp.json", + &sha256_hash.as_ref(), + ) + .await? + } else if let Some(sha512_hash) = &hashes.sha512 { + fetch_sha512( + transport, + url.clone(), + snapshot_meta.length.unwrap_or(max_snapshot_size), + "timestamp.json", + &sha512_hash.as_ref(), + ) + .await? + } else { + fetch_max_size( + transport, + url.clone(), + snapshot_meta.length.unwrap_or(max_snapshot_size), + "timestamp.json", + ) + .await? + } } else { fetch_max_size( transport, @@ -1112,14 +1140,30 @@ async fn load_targets( None => (max_targets_size, "max_targets_size parameter"), }; let stream = if let Some(hashes) = &targets_meta.hashes { - fetch_sha256( - transport, - targets_url.clone(), - max_targets_size, - specifier, - &hashes.sha256, - ) - .await? + if let Some(sha256_hash) = &hashes.sha256 { + fetch_sha256( + transport, + targets_url.clone(), + max_targets_size, + specifier, + sha256_hash.as_ref(), + ) + .await? + } else if let Some(sha512_hash) = &hashes.sha512 { + fetch_sha512( + transport, + targets_url.clone(), + max_targets_size, + specifier, + sha512_hash.as_ref(), + ) + .await? + } else { + error::NoValidHashSnafu { + name: targets_url.path().to_string(), + } + .fail()? + } } else { fetch_max_size(transport, targets_url.clone(), max_targets_size, specifier).await? }; diff --git a/tough/src/schema/mod.rs b/tough/src/schema/mod.rs index 72efa3328..89709c8e7 100644 --- a/tough/src/schema/mod.rs +++ b/tough/src/schema/mod.rs @@ -21,7 +21,7 @@ use chrono::{DateTime, Utc}; use globset::{Glob, GlobMatcher}; use hex::ToHex; use olpc_cjson::CanonicalFormatter; -use ring::digest::{digest, Context, SHA256}; +use ring::digest::{digest, Context, SHA256, SHA512}; use serde::de::Error as SerdeDeError; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; @@ -324,7 +324,11 @@ pub struct Metafile { #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] pub struct Hashes { /// The SHA 256 digest of a metadata file. - pub sha256: Decoded, + #[serde(skip_serializing_if = "Option::is_none")] + pub sha256: Option>, + /// The SHA 512 digest of a metadata file. + #[serde(skip_serializing_if = "Option::is_none")] + pub sha512: Option>, /// Extra arguments found during deserialization. /// @@ -465,11 +469,13 @@ impl Target { return error::TargetNotAFileSnafu { path }.fail(); } - // Get the sha256 and length of the target + // Get the sha256 (and sha512) and length of the target let mut file = File::open(path) .await .context(error::FileOpenSnafu { path })?; - let mut digest = Context::new(&SHA256); + + let mut sha256_digest = Context::new(&SHA256); + let mut sha512_digest = Context::new(&SHA512); let mut buf = [0; 8 * 1024]; let mut length = 0; loop { @@ -480,7 +486,8 @@ impl Target { { 0 => break, n => { - digest.update(&buf[..n]); + sha256_digest.update(&buf[..n]); + sha512_digest.update(&buf[..n]); length += n as u64; } } @@ -489,7 +496,8 @@ impl Target { Ok(Target { length, hashes: Hashes { - sha256: Decoded::from(digest.finish().as_ref().to_vec()), + sha256: Some(Decoded::from(sha256_digest.finish().as_ref().to_vec())), + sha512: Some(Decoded::from(sha512_digest.finish().as_ref().to_vec())), _extra: HashMap::new(), }, custom: HashMap::new(), @@ -1161,7 +1169,8 @@ fn targets_iter_and_map_test() { let nothing = Target { length: 0, hashes: Hashes { - sha256: [0u8].to_vec().into(), + sha256: Some([0u8].to_vec().into()), + sha512: None, _extra: HashMap::default(), }, custom: HashMap::default(),