blob: 04f9d2c6ab61e2d442652732ddc03c8bede87d49 [file] [log] [blame]
//! Interfaces for interacting with different types of TUF repositories.
use hyper::{Url, Client};
use hyper::client::response::Response;
use hyper::header::{Headers, UserAgent};
use hyper::status::StatusCode;
use ring::digest::{self, SHA256, SHA512};
use std::collections::HashMap;
use std::fs::{self, File, DirBuilder};
use std::io::{Read, Write, Cursor};
use std::marker::PhantomData;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use Result;
use crypto::{self, HashAlgorithm, HashValue};
use error::Error;
use metadata::{SignedMetadata, MetadataVersion, Role, Metadata, TargetPath, TargetDescription,
MetadataPath};
use interchange::DataInterchange;
/// Top-level trait that represents a TUF repository and contains all the ways it can be interacted
/// with.
pub trait Repository<D>
where
D: DataInterchange,
{
/// The type returned when reading a target.
type TargetRead: Read;
/// Initialize the repository.
fn initialize(&mut self) -> Result<()>;
/// Store signed metadata.
fn store_metadata<M>(
&mut self,
role: &Role,
meta_path: &MetadataPath,
version: &MetadataVersion,
metadata: &SignedMetadata<D, M>,
) -> Result<()>
where
M: Metadata;
/// Fetch signed metadata.
fn fetch_metadata<M>(
&mut self,
role: &Role,
meta_path: &MetadataPath,
version: &MetadataVersion,
max_size: &Option<usize>,
hash_data: Option<(&HashAlgorithm, &HashValue)>,
) -> Result<SignedMetadata<D, M>>
where
M: Metadata;
/// Store the given target.
fn store_target<R>(
&mut self,
read: R,
target_path: &TargetPath,
target_description: &TargetDescription,
) -> Result<()>
where
R: Read;
/// Fetch the given target.
///
/// **WARNING**: The target will **NOT** yet be verified.
fn fetch_target(&mut self, target_path: &TargetPath) -> Result<Self::TargetRead>;
/// Perform a sanity check that `M`, `Role`, and `MetadataPath` all desrcribe the same entity.
fn check<M>(role: &Role, meta_path: &MetadataPath) -> Result<()>
where
M: Metadata,
{
if role != &M::role() {
return Err(Error::IllegalArgument(format!(
"Attempted to store {} metadata as {}.",
M::role(),
role
)));
}
if !role.fuzzy_matches_path(meta_path) {
return Err(Error::IllegalArgument(
format!("Role {} does not match path {:?}", role, meta_path),
));
}
Ok(())
}
/// Read the from given reader, optionally capped at `max_size` bytes, optionally requiring
/// hashes to match.
fn safe_read<R, W>(
mut read: R,
mut write: 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(|_| ())?,
}
}
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::Programming(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::Programming(msg.into()))
}
(Some(_), Some(_)) |
(None, None) => Ok(()),
}
}
}
/// A repository contained on the local file system.
pub struct FileSystemRepository<D>
where
D: DataInterchange,
{
local_path: PathBuf,
_interchange: PhantomData<D>,
}
impl<D> FileSystemRepository<D>
where
D: DataInterchange,
{
/// Create a new repository on the local file system.
pub fn new(local_path: PathBuf) -> Self {
FileSystemRepository {
local_path: local_path,
_interchange: PhantomData,
}
}
}
impl<D> Repository<D> for FileSystemRepository<D>
where
D: DataInterchange,
{
type TargetRead = File;
fn initialize(&mut self) -> Result<()> {
for p in &["metadata", "targets", "temp"] {
DirBuilder::new().recursive(true).create(
self.local_path.join(p),
)?
}
Ok(())
}
fn store_metadata<M>(
&mut self,
role: &Role,
meta_path: &MetadataPath,
version: &MetadataVersion,
metadata: &SignedMetadata<D, M>,
) -> Result<()>
where
M: Metadata,
{
Self::check::<M>(role, meta_path)?;
let mut path = self.local_path.join("metadata");
path.extend(meta_path.components::<D>(version));
if path.exists() {
debug!("Metadata path exists. Deleting: {:?}", path);
fs::remove_file(&path)?
}
let mut file = File::create(&path)?;
D::to_writer(&mut file, metadata)?;
Ok(())
}
/// Fetch signed metadata.
fn fetch_metadata<M>(
&mut self,
role: &Role,
meta_path: &MetadataPath,
version: &MetadataVersion,
max_size: &Option<usize>,
hash_data: Option<(&HashAlgorithm, &HashValue)>,
) -> Result<SignedMetadata<D, M>>
where
M: Metadata,
{
Self::check::<M>(role, meta_path)?;
let mut path = self.local_path.join("metadata");
path.extend(meta_path.components::<D>(&version));
let mut file = File::open(&path)?;
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)?)
}
fn store_target<R>(
&mut self,
read: R,
target_path: &TargetPath,
target_description: &TargetDescription,
) -> Result<()>
where
R: Read,
{
let mut temp_file = NamedTempFile::new_in(self.local_path.join("temp"))?;
let hash_data = crypto::hash_preference(target_description.hashes())?;
Self::safe_read(
read,
&mut temp_file,
Some(target_description.length() as i64),
Some(hash_data),
)?;
let mut path = self.local_path.clone().join("targets");
path.extend(target_path.components());
temp_file.persist(&path)?;
Ok(())
}
fn fetch_target(&mut self, target_path: &TargetPath) -> Result<File> {
let mut path = self.local_path.join("targets");
path.extend(target_path.components());
if !path.exists() {
return Err(Error::NotFound);
}
Ok(File::open(&path)?)
}
}
/// A repository accessible over HTTP.
pub struct HttpRepository<D>
where
D: DataInterchange,
{
url: Url,
client: Client,
user_agent: String,
_interchange: PhantomData<D>,
}
impl<D> HttpRepository<D>
where
D: DataInterchange,
{
/// Create a new repository with the given `Url` and `Client`. Callers *should* include a
/// custom User-Agent prefix to maintainers of TUF repositories keep track of which client
/// versions exist in the field.
pub fn new(url: Url, client: Client, user_agent_prefix: Option<String>) -> Self {
let user_agent = match user_agent_prefix {
Some(ua) => format!("{} (rust-tuf/{})", ua, env!("CARGO_PKG_VERSION")),
None => format!("rust-tuf/{}", env!("CARGO_PKG_VERSION")),
};
HttpRepository {
url: url,
client: client,
user_agent: user_agent,
_interchange: PhantomData,
}
}
fn get(&self, components: &[String]) -> Result<Response> {
let mut headers = Headers::new();
headers.set(UserAgent(self.user_agent.clone()));
let mut url = self.url.clone();
url.path_segments_mut()
.map_err(|_| {
Error::IllegalArgument(format!("URL was 'cannot-be-a-base': {:?}", self.url))
})?
.extend(components);
let req = self.client.get(url.clone()).headers(headers);
let resp = req.send()?;
if !resp.status.is_success() {
if resp.status == StatusCode::NotFound {
Err(Error::NotFound)
} else {
Err(Error::Opaque(
format!("Error getting {:?}: {:?}", url, resp),
))
}
} else {
Ok(resp)
}
}
}
impl<D> Repository<D> for HttpRepository<D>
where
D: DataInterchange,
{
type TargetRead = Response;
fn initialize(&mut self) -> Result<()> {
Ok(())
}
/// This always returns `Err` as storing over HTTP is not yet supported.
fn store_metadata<M>(
&mut self,
_: &Role,
_: &MetadataPath,
_: &MetadataVersion,
_: &SignedMetadata<D, M>,
) -> Result<()>
where
M: Metadata,
{
Err(Error::Opaque(
"Http repo store metadata not implemented".to_string(),
))
}
fn fetch_metadata<M>(
&mut self,
role: &Role,
meta_path: &MetadataPath,
version: &MetadataVersion,
max_size: &Option<usize>,
hash_data: Option<(&HashAlgorithm, &HashValue)>,
) -> Result<SignedMetadata<D, M>>
where
M: Metadata,
{
Self::check::<M>(role, meta_path)?;
let mut resp = self.get(&meta_path.components::<D>(&version))?;
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)?)
}
/// This always returns `Err` as storing over HTTP is not yet supported.
fn store_target<R>(&mut self, _: R, _: &TargetPath, _: &TargetDescription) -> Result<()>
where
R: Read,
{
Err(Error::Opaque(
"Http repo store not implemented".to_string(),
))
}
fn fetch_target(&mut self, target_path: &TargetPath) -> Result<Self::TargetRead> {
let resp = self.get(&target_path.components())?;
Ok(resp)
}
}
/// An ephemeral repository contained solely in memory.
pub struct EphemeralRepository<D>
where
D: DataInterchange,
{
metadata: HashMap<(MetadataPath, MetadataVersion), Vec<u8>>,
targets: HashMap<TargetPath, Vec<u8>>,
_interchange: PhantomData<D>,
}
impl<D> EphemeralRepository<D>
where
D: DataInterchange,
{
/// Create a new ephemercal repository.
pub fn new() -> Self {
EphemeralRepository {
metadata: HashMap::new(),
targets: HashMap::new(),
_interchange: PhantomData,
}
}
}
impl<D> Repository<D> for EphemeralRepository<D>
where
D: DataInterchange,
{
type TargetRead = Cursor<Vec<u8>>;
fn initialize(&mut self) -> Result<()> {
Ok(())
}
fn store_metadata<M>(
&mut self,
role: &Role,
meta_path: &MetadataPath,
version: &MetadataVersion,
metadata: &SignedMetadata<D, M>,
) -> Result<()>
where
M: Metadata,
{
Self::check::<M>(role, meta_path)?;
let mut buf = Vec::new();
D::to_writer(&mut buf, metadata)?;
let _ = self.metadata.insert(
(meta_path.clone(), version.clone()),
buf,
);
Ok(())
}
fn fetch_metadata<M>(
&mut self,
role: &Role,
meta_path: &MetadataPath,
version: &MetadataVersion,
max_size: &Option<usize>,
hash_data: Option<(&HashAlgorithm, &HashValue)>,
) -> Result<SignedMetadata<D, M>>
where
M: Metadata,
{
Self::check::<M>(role, meta_path)?;
match self.metadata.get(&(meta_path.clone(), version.clone())) {
Some(bytes) => {
let mut buf = Vec::new();
Self::safe_read(
bytes.as_slice(),
&mut buf,
max_size.map(|x| x as i64),
hash_data,
)?;
D::from_reader(&*buf)
}
None => Err(Error::NotFound),
}
}
fn store_target<R>(
&mut self,
read: R,
target_path: &TargetPath,
target_description: &TargetDescription,
) -> Result<()>
where
R: Read,
{
let mut buf = Vec::new();
let hash_data = crypto::hash_preference(target_description.hashes())?;
Self::safe_read(
read,
&mut buf,
Some(target_description.length() as i64),
Some(hash_data),
)?;
let _ = self.targets.insert(target_path.clone(), buf);
Ok(())
}
fn fetch_target(&mut self, target_path: &TargetPath) -> Result<Self::TargetRead> {
match self.targets.get(target_path) {
Some(bytes) => Ok(Cursor::new(bytes.clone())),
None => Err(Error::NotFound),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use tempdir::TempDir;
use interchange::JsonDataInterchange;
#[test]
fn ephemeral_repo_targets() {
let mut repo = EphemeralRepository::<JsonDataInterchange>::new();
repo.initialize().expect("initialize repo");
let data: &[u8] = b"like tears in the rain";
let target_description =
TargetDescription::from_reader(data).expect("generate target description");
let path = TargetPath::new("batty".into()).expect("make target path");
repo.store_target(data, &path, &target_description).expect(
"store target",
);
let mut read = repo.fetch_target(&path).expect("fetch target");
let mut buf = Vec::new();
read.read_to_end(&mut buf).expect("read target");
assert_eq!(buf.as_slice(), data);
let bad_data: &[u8] = b"you're in a desert";
assert!(
repo.store_target(bad_data, &path, &target_description)
.is_err()
);
let mut read = repo.fetch_target(&path).expect("fetch target");
let mut buf = Vec::new();
read.read_to_end(&mut buf).expect("read target");
assert_eq!(buf.as_slice(), data);
}
#[test]
fn file_system_repo_targets() {
let temp_dir = TempDir::new("rust-tuf").expect("make temp dir");
let mut repo =
FileSystemRepository::<JsonDataInterchange>::new(temp_dir.path().to_path_buf());
repo.initialize().expect("initialize repo");
let data: &[u8] = b"like tears in the rain";
let target_description =
TargetDescription::from_reader(data).expect("generate target desert");
let path = TargetPath::new("batty".into()).expect("make target path");
repo.store_target(data, &path, &target_description).expect(
"store target",
);
assert!(temp_dir.path().join("targets").join("batty").exists());
let mut read = repo.fetch_target(&path).expect("fetch target");
let mut buf = Vec::new();
read.read_to_end(&mut buf).expect("read target");
assert_eq!(buf.as_slice(), data);
let bad_data: &[u8] = b"you're in a desert";
assert!(
repo.store_target(bad_data, &path, &target_description)
.is_err()
);
let mut read = repo.fetch_target(&path).expect("fetch target");
let mut buf = Vec::new();
read.read_to_end(&mut buf).expect("read target");
assert_eq!(buf.as_slice(), data);
}
}