use {
crate::{
crypto::{self, HashAlgorithm, PrivateKey},
database::Database,
interchange::DataInterchange,
metadata::{
Metadata, MetadataDescription, MetadataPath, MetadataVersion, RawSignedMetadata,
RawSignedMetadataSet, RawSignedMetadataSetBuilder, Role, RootMetadata,
RootMetadataBuilder, SignedMetadataBuilder, SnapshotMetadata, SnapshotMetadataBuilder,
TargetDescription, TargetPath, TargetsMetadata, TargetsMetadataBuilder,
TimestampMetadata, TimestampMetadataBuilder,
},
repository::RepositoryStorage,
Error, Result,
},
chrono::{Duration, Utc},
futures_io::{AsyncRead, AsyncSeek},
futures_util::AsyncSeekExt as _,
std::{collections::HashMap, io::SeekFrom, marker::PhantomData},
};
mod private {
use super::*;
pub trait Sealed {}
impl Sealed for Root {}
impl<D: DataInterchange> Sealed for Targets<D> {}
impl<D: DataInterchange> Sealed for Snapshot<D> {}
impl<D: DataInterchange> Sealed for Timestamp<D> {}
impl<D: DataInterchange> Sealed for Done<D> {}
}
pub trait State: private::Sealed {}
#[doc(hidden)]
pub struct Root {
builder: RootMetadataBuilder,
}
impl State for Root {}
#[doc(hidden)]
pub struct Targets<D: DataInterchange> {
staged_root: Option<Staged<D, RootMetadata>>,
builder: TargetsMetadataBuilder,
file_hash_algorithms: Vec<HashAlgorithm>,
}
impl<D: DataInterchange> Targets<D> {
fn new(staged_root: Option<Staged<D, RootMetadata>>) -> Self {
Self {
staged_root,
builder: TargetsMetadataBuilder::new().expires(Utc::now() + Duration::days(90)),
file_hash_algorithms: vec![HashAlgorithm::Sha256],
}
}
}
impl<D: DataInterchange> State for Targets<D> {}
#[doc(hidden)]
pub struct Snapshot<D: DataInterchange> {
staged_root: Option<Staged<D, RootMetadata>>,
staged_targets: Option<Staged<D, TargetsMetadata>>,
include_targets_length: bool,
targets_hash_algorithms: Vec<HashAlgorithm>,
inherit_targets: bool,
}
impl<D: DataInterchange> State for Snapshot<D> {}
impl<D: DataInterchange> Snapshot<D> {
fn new(
staged_root: Option<Staged<D, RootMetadata>>,
staged_targets: Option<Staged<D, TargetsMetadata>>,
) -> Self {
Self {
staged_root,
staged_targets,
include_targets_length: false,
targets_hash_algorithms: vec![],
inherit_targets: true,
}
}
fn targets_description(&self) -> Result<Option<MetadataDescription>> {
if let Some(ref targets) = self.staged_targets {
let length = if self.include_targets_length {
Some(targets.raw.as_bytes().len())
} else {
None
};
let hashes = if self.targets_hash_algorithms.is_empty() {
HashMap::new()
} else {
crypto::calculate_hashes_from_slice(
targets.raw.as_bytes(),
&self.targets_hash_algorithms,
)?
};
Ok(Some(MetadataDescription::new(
targets.metadata.version(),
length,
hashes,
)?))
} else {
Ok(None)
}
}
}
pub struct Timestamp<D: DataInterchange> {
staged_root: Option<Staged<D, RootMetadata>>,
staged_targets: Option<Staged<D, TargetsMetadata>>,
staged_snapshot: Option<Staged<D, SnapshotMetadata>>,
include_snapshot_length: bool,
snapshot_hash_algorithms: Vec<HashAlgorithm>,
}
impl<D: DataInterchange> Timestamp<D> {
fn new(state: Snapshot<D>, staged_snapshot: Option<Staged<D, SnapshotMetadata>>) -> Self {
Self {
staged_root: state.staged_root,
staged_targets: state.staged_targets,
staged_snapshot,
include_snapshot_length: false,
snapshot_hash_algorithms: vec![],
}
}
fn snapshot_description(&self) -> Result<Option<MetadataDescription>> {
if let Some(ref snapshot) = self.staged_snapshot {
let length = if self.include_snapshot_length {
Some(snapshot.raw.as_bytes().len())
} else {
None
};
let hashes = if self.snapshot_hash_algorithms.is_empty() {
HashMap::new()
} else {
crypto::calculate_hashes_from_slice(
snapshot.raw.as_bytes(),
&self.snapshot_hash_algorithms,
)?
};
Ok(Some(MetadataDescription::new(
snapshot.metadata.version(),
length,
hashes,
)?))
} else {
Ok(None)
}
}
}
impl<D: DataInterchange> State for Timestamp<D> {}
pub struct Done<D: DataInterchange> {
staged_root: Option<Staged<D, RootMetadata>>,
staged_targets: Option<Staged<D, TargetsMetadata>>,
staged_snapshot: Option<Staged<D, SnapshotMetadata>>,
staged_timestamp: Option<Staged<D, TimestampMetadata>>,
}
impl<D: DataInterchange> State for Done<D> {}
struct Staged<D: DataInterchange, M: Metadata> {
metadata: M,
raw: RawSignedMetadata<D, M>,
}
struct RepoContext<'a, D, R>
where
D: DataInterchange + Sync,
R: RepositoryStorage<D>,
{
repo: R,
db: Option<&'a Database<D>>,
signing_root_keys: Vec<&'a dyn PrivateKey>,
signing_targets_keys: Vec<&'a dyn PrivateKey>,
signing_snapshot_keys: Vec<&'a dyn PrivateKey>,
signing_timestamp_keys: Vec<&'a dyn PrivateKey>,
trusted_root_keys: Vec<&'a dyn PrivateKey>,
trusted_targets_keys: Vec<&'a dyn PrivateKey>,
trusted_snapshot_keys: Vec<&'a dyn PrivateKey>,
trusted_timestamp_keys: Vec<&'a dyn PrivateKey>,
_interchange: PhantomData<D>,
}
fn sign<'a, D, I, M>(meta: &M, keys: I) -> Result<RawSignedMetadata<D, M>>
where
D: DataInterchange,
M: Metadata,
I: IntoIterator<Item = &'a &'a dyn PrivateKey>,
{
let mut signed_builder = SignedMetadataBuilder::<D, _>::from_metadata(meta)?;
for key in keys {
signed_builder = signed_builder.sign(*key)?;
}
signed_builder.build().to_raw()
}
pub struct RepoBuilder<'a, D, R, S = Root>
where
D: DataInterchange + Sync,
R: RepositoryStorage<D>,
S: State,
{
ctx: RepoContext<'a, D, R>,
state: S,
}
impl<'a, D, R> RepoBuilder<'a, D, R, Root>
where
D: DataInterchange + Sync,
R: RepositoryStorage<D>,
{
pub fn create(repo: R) -> Self {
Self {
ctx: RepoContext {
repo,
db: None,
signing_root_keys: vec![],
signing_targets_keys: vec![],
signing_snapshot_keys: vec![],
signing_timestamp_keys: vec![],
trusted_root_keys: vec![],
trusted_targets_keys: vec![],
trusted_snapshot_keys: vec![],
trusted_timestamp_keys: vec![],
_interchange: PhantomData,
},
state: Root {
builder: RootMetadataBuilder::new()
.consistent_snapshot(true)
.root_threshold(1)
.targets_threshold(1)
.snapshot_threshold(1)
.timestamp_threshold(1),
},
}
}
pub fn from_database(repo: R, db: &'a Database<D>) -> Self {
let builder = {
let trusted_root = db.trusted_root();
RootMetadataBuilder::new()
.consistent_snapshot(trusted_root.consistent_snapshot())
.root_threshold(trusted_root.root().threshold())
.targets_threshold(trusted_root.targets().threshold())
.snapshot_threshold(trusted_root.snapshot().threshold())
.timestamp_threshold(trusted_root.timestamp().threshold())
};
Self {
ctx: RepoContext {
repo,
db: Some(db),
signing_root_keys: vec![],
signing_targets_keys: vec![],
signing_snapshot_keys: vec![],
signing_timestamp_keys: vec![],
trusted_root_keys: vec![],
trusted_targets_keys: vec![],
trusted_snapshot_keys: vec![],
trusted_timestamp_keys: vec![],
_interchange: PhantomData,
},
state: Root { builder },
}
}
pub fn signing_root_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self {
for key in keys {
self.ctx.signing_root_keys.push(*key);
}
self
}
pub fn signing_targets_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self {
for key in keys {
self.ctx.signing_targets_keys.push(*key);
}
self
}
pub fn signing_snapshot_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self {
for key in keys {
self.ctx.signing_snapshot_keys.push(*key);
}
self
}
pub fn signing_timestamp_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self {
for key in keys {
self.ctx.signing_timestamp_keys.push(*key);
}
self
}
pub fn trusted_root_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self {
for key in keys {
self.ctx.trusted_root_keys.push(*key);
self.state.builder = self.state.builder.root_key(key.public().clone());
}
self
}
pub fn trusted_targets_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self {
for key in keys {
self.ctx.trusted_targets_keys.push(*key);
self.state.builder = self.state.builder.targets_key(key.public().clone());
}
self
}
pub fn trusted_snapshot_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self {
for key in keys {
self.ctx.trusted_snapshot_keys.push(*key);
self.state.builder = self.state.builder.snapshot_key(key.public().clone());
}
self
}
pub fn trusted_timestamp_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self {
for key in keys {
self.ctx.trusted_timestamp_keys.push(*key);
self.state.builder = self.state.builder.timestamp_key(key.public().clone());
}
self
}
pub fn stage_root(self) -> Result<RepoBuilder<'a, D, R, Targets<D>>> {
self.stage_root_with_builder(|builder| builder)
}
pub fn stage_root_if_necessary(self) -> Result<RepoBuilder<'a, D, R, Targets<D>>> {
if self.need_new_root() {
self.stage_root()
} else {
Ok(self.skip_root())
}
}
pub fn skip_root(self) -> RepoBuilder<'a, D, R, Targets<D>> {
RepoBuilder {
ctx: self.ctx,
state: Targets::new(None),
}
}
pub fn stage_root_with_builder<F>(self, f: F) -> Result<RepoBuilder<'a, D, R, Targets<D>>>
where
F: FnOnce(RootMetadataBuilder) -> RootMetadataBuilder,
{
let next_version = if let Some(db) = self.ctx.db {
db.trusted_root().version().checked_add(1).ok_or_else(|| {
Error::VerificationFailure("root version should be less than max u32".into())
})?
} else {
1
};
let root_builder = self
.state
.builder
.version(next_version)
.expires(Utc::now() + Duration::days(365));
let root = f(root_builder).build()?;
let raw_root = sign(
&root,
self.ctx
.signing_root_keys
.iter()
.chain(&self.ctx.trusted_root_keys),
)?;
Ok(RepoBuilder {
ctx: self.ctx,
state: Targets::new(Some(Staged {
metadata: root,
raw: raw_root,
})),
})
}
pub async fn add_target<Rd>(
self,
target_path: TargetPath,
reader: Rd,
) -> Result<RepoBuilder<'a, D, R, Targets<D>>>
where
Rd: AsyncRead + AsyncSeek + Unpin + Send,
{
self.stage_root_if_necessary()?
.add_target(target_path, reader)
.await
}
pub async fn commit(self) -> Result<RawSignedMetadataSet<D>> {
self.stage_root_if_necessary()?.commit().await
}
fn need_new_root(&self) -> bool {
let root = if let Some(db) = self.ctx.db {
db.trusted_root()
} else {
return true;
};
if root.expires() <= &Utc::now() {
return true;
}
if !self.ctx.signing_root_keys.is_empty() {
return true;
}
let root_keys_count = root.root_keys().count();
if root_keys_count != self.ctx.trusted_root_keys.len() {
return true;
}
for &key in &self.ctx.trusted_root_keys {
if !root.root_keys().any(|x| x == key.public()) {
return true;
}
}
for &key in &self.ctx.trusted_targets_keys {
if !root.targets_keys().any(|x| x == key.public()) {
return true;
}
}
for &key in &self.ctx.trusted_snapshot_keys {
if !root.snapshot_keys().any(|x| x == key.public()) {
return true;
}
}
for &key in &self.ctx.trusted_timestamp_keys {
if !root.timestamp_keys().any(|x| x == key.public()) {
return true;
}
}
false
}
}
impl<'a, D, R> RepoBuilder<'a, D, R, Targets<D>>
where
D: DataInterchange + Sync,
R: RepositoryStorage<D>,
{
pub fn file_hash_algorithms(mut self, algorithms: &[HashAlgorithm]) -> Self {
self.state.file_hash_algorithms = algorithms.to_vec();
self
}
pub fn stage_targets(self) -> Result<RepoBuilder<'a, D, R, Snapshot<D>>> {
self.stage_targets_with_builder(|builder| builder)
}
pub fn stage_targets_if_necessary(self) -> Result<RepoBuilder<'a, D, R, Snapshot<D>>> {
if self.need_new_targets() {
self.stage_targets_with_builder(|builder| builder)
} else {
Ok(self.skip_targets())
}
}
pub fn skip_targets(self) -> RepoBuilder<'a, D, R, Snapshot<D>> {
RepoBuilder {
ctx: self.ctx,
state: Snapshot::new(self.state.staged_root, None),
}
}
pub async fn add_target<Rd>(
mut self,
target_path: TargetPath,
mut reader: Rd,
) -> Result<RepoBuilder<'a, D, R, Targets<D>>>
where
Rd: AsyncRead + AsyncSeek + Unpin + Send,
{
let consistent_snapshot = if let Some(ref staged_root) = self.state.staged_root {
staged_root.metadata.consistent_snapshot()
} else if let Some(db) = self.ctx.db {
db.trusted_root().consistent_snapshot()
} else {
return Err(Error::MissingMetadata(Role::Root));
};
let target_description =
TargetDescription::from_reader(&mut reader, &self.state.file_hash_algorithms).await?;
if consistent_snapshot {
for digest in target_description.hashes().values() {
reader.seek(SeekFrom::Start(0)).await?;
let hash_prefixed_path = target_path.with_hash_prefix(digest)?;
self.ctx
.repo
.store_target(&hash_prefixed_path, &mut reader)
.await?;
}
} else {
reader.seek(SeekFrom::Start(0)).await?;
self.ctx
.repo
.store_target(&target_path, &mut reader)
.await?;
}
self.state.builder = self
.state
.builder
.insert_target_description(target_path, target_description);
Ok(self)
}
pub fn stage_targets_with_builder<F>(self, f: F) -> Result<RepoBuilder<'a, D, R, Snapshot<D>>>
where
F: FnOnce(TargetsMetadataBuilder) -> TargetsMetadataBuilder,
{
let next_version = if let Some(db) = self.ctx.db {
if let Some(trusted_targets) = db.trusted_targets() {
trusted_targets.version().checked_add(1).ok_or_else(|| {
Error::VerificationFailure("targets version should be less than max u32".into())
})?
} else {
1
}
} else {
1
};
let targets_builder = self.state.builder.version(next_version);
let targets = f(targets_builder).build()?;
let raw_targets = sign(
&targets,
self.ctx
.signing_targets_keys
.iter()
.chain(&self.ctx.trusted_targets_keys),
)?;
Ok(RepoBuilder {
ctx: self.ctx,
state: Snapshot::new(
self.state.staged_root,
Some(Staged {
metadata: targets,
raw: raw_targets,
}),
),
})
}
pub async fn commit(self) -> Result<RawSignedMetadataSet<D>> {
self.stage_targets_if_necessary()?.commit().await
}
#[cfg(test)]
pub async fn commit_skip_validation(self) -> Result<RawSignedMetadataSet<D>> {
self.stage_targets_if_necessary()?
.commit_skip_validation()
.await
}
fn need_new_targets(&self) -> bool {
let db = if let Some(ref db) = self.ctx.db {
db
} else {
return true;
};
if let Some(targets) = db.trusted_targets() {
if targets.expires() <= &Utc::now() {
return true;
}
} else {
return true;
}
false
}
}
impl<'a, D, R> RepoBuilder<'a, D, R, Snapshot<D>>
where
D: DataInterchange + Sync,
R: RepositoryStorage<D>,
{
pub fn snapshot_includes_length(mut self, include_targets_lengths: bool) -> Self {
self.state.include_targets_length = include_targets_lengths;
self
}
pub fn snapshot_includes_hashes(mut self, hashes: &[HashAlgorithm]) -> Self {
self.state.targets_hash_algorithms = hashes.to_vec();
self
}
pub fn inherit_targets(mut self, inherit_targets: bool) -> Self {
self.state.inherit_targets = inherit_targets;
self
}
pub fn stage_snapshot(self) -> Result<RepoBuilder<'a, D, R, Timestamp<D>>> {
self.stage_snapshot_with_builder(|builder| builder)
}
pub fn stage_snapshot_if_necessary(self) -> Result<RepoBuilder<'a, D, R, Timestamp<D>>> {
if self.need_new_snapshot() {
self.stage_snapshot()
} else {
Ok(self.skip_snapshot())
}
}
pub fn skip_snapshot(self) -> RepoBuilder<'a, D, R, Timestamp<D>> {
RepoBuilder {
ctx: self.ctx,
state: Timestamp::new(self.state, None),
}
}
pub fn stage_snapshot_with_builder<F>(self, f: F) -> Result<RepoBuilder<'a, D, R, Timestamp<D>>>
where
F: FnOnce(SnapshotMetadataBuilder) -> SnapshotMetadataBuilder,
{
let next_version = if let Some(db) = self.ctx.db {
if let Some(trusted_snapshot) = db.trusted_snapshot() {
trusted_snapshot.version().checked_add(1).ok_or_else(|| {
Error::VerificationFailure(
"snapshot version should be less than max u32".into(),
)
})?
} else {
1
}
} else {
1
};
let mut snapshot_builder = SnapshotMetadataBuilder::new()
.version(next_version)
.expires(Utc::now() + Duration::days(7));
if self.state.inherit_targets {
if let Some(db) = self.ctx.db {
if let Some(snapshot) = db.trusted_snapshot() {
for (path, description) in snapshot.meta() {
snapshot_builder = snapshot_builder
.insert_metadata_description(path.clone(), description.clone());
}
}
}
}
if let Some(targets_description) = self.state.targets_description()? {
snapshot_builder = snapshot_builder.insert_metadata_description(
MetadataPath::from_role(&Role::Targets),
targets_description,
);
};
let snapshot = f(snapshot_builder).build()?;
let raw_snapshot = sign(
&snapshot,
self.ctx
.signing_snapshot_keys
.iter()
.chain(&self.ctx.trusted_snapshot_keys),
)?;
Ok(RepoBuilder {
ctx: self.ctx,
state: Timestamp::new(
self.state,
Some(Staged {
metadata: snapshot,
raw: raw_snapshot,
}),
),
})
}
pub async fn commit(self) -> Result<RawSignedMetadataSet<D>> {
self.stage_snapshot_if_necessary()?.commit().await
}
#[cfg(test)]
pub async fn commit_skip_validation(self) -> Result<RawSignedMetadataSet<D>> {
self.stage_snapshot_if_necessary()?
.commit_skip_validation()
.await
}
fn need_new_snapshot(&self) -> bool {
let db = if let Some(ref db) = self.ctx.db {
db
} else {
return true;
};
if let Some(snapshot) = db.trusted_snapshot() {
if snapshot.expires() <= &Utc::now() {
return true;
}
} else {
return true;
}
false
}
}
impl<'a, D, R> RepoBuilder<'a, D, R, Timestamp<D>>
where
D: DataInterchange + Sync,
R: RepositoryStorage<D>,
{
pub fn timestamp_includes_length(mut self, include_snapshot_lengths: bool) -> Self {
self.state.include_snapshot_length = include_snapshot_lengths;
self
}
pub fn timestamp_includes_hashes(mut self, hashes: &[HashAlgorithm]) -> Self {
self.state.snapshot_hash_algorithms = hashes.to_vec();
self
}
pub fn stage_timestamp(self) -> Result<RepoBuilder<'a, D, R, Done<D>>> {
self.with_timestamp_builder(|builder| builder)
}
pub fn stage_timestamp_if_necessary(self) -> Result<RepoBuilder<'a, D, R, Done<D>>> {
if self.need_new_timestamp() {
self.stage_timestamp()
} else {
Ok(self.skip_timestamp())
}
}
pub fn skip_timestamp(self) -> RepoBuilder<'a, D, R, Done<D>> {
RepoBuilder {
ctx: self.ctx,
state: Done {
staged_root: self.state.staged_root,
staged_targets: self.state.staged_targets,
staged_snapshot: self.state.staged_snapshot,
staged_timestamp: None,
},
}
}
pub fn with_timestamp_builder<F>(self, f: F) -> Result<RepoBuilder<'a, D, R, Done<D>>>
where
F: FnOnce(TimestampMetadataBuilder) -> TimestampMetadataBuilder,
{
let next_version = if let Some(db) = self.ctx.db {
if let Some(trusted_timestamp) = db.trusted_timestamp() {
trusted_timestamp.version().checked_add(1).ok_or_else(|| {
Error::VerificationFailure(
"timestamp version should be less than max u32".into(),
)
})?
} else {
1
}
} else {
1
};
let description = if let Some(description) = self.state.snapshot_description()? {
description
} else {
self.ctx
.db
.and_then(|db| db.trusted_timestamp())
.map(|timestamp| timestamp.snapshot().clone())
.ok_or(Error::MissingMetadata(Role::Timestamp))?
};
let timestamp_builder = TimestampMetadataBuilder::from_metadata_description(description)
.version(next_version)
.expires(Utc::now() + Duration::days(1));
let timestamp = f(timestamp_builder).build()?;
let raw_timestamp = sign(
×tamp,
self.ctx
.signing_timestamp_keys
.iter()
.chain(&self.ctx.trusted_timestamp_keys),
)?;
Ok(RepoBuilder {
ctx: self.ctx,
state: Done {
staged_root: self.state.staged_root,
staged_targets: self.state.staged_targets,
staged_snapshot: self.state.staged_snapshot,
staged_timestamp: Some(Staged {
metadata: timestamp,
raw: raw_timestamp,
}),
},
})
}
pub async fn commit(self) -> Result<RawSignedMetadataSet<D>> {
self.stage_timestamp_if_necessary()?.commit().await
}
#[cfg(test)]
pub async fn commit_skip_validation(self) -> Result<RawSignedMetadataSet<D>> {
self.stage_timestamp_if_necessary()?
.commit_skip_validation()
.await
}
fn need_new_timestamp(&self) -> bool {
let db = if let Some(ref db) = self.ctx.db {
db
} else {
return true;
};
if let Some(timestamp) = db.trusted_timestamp() {
if timestamp.expires() <= &Utc::now() {
return true;
}
} else {
return true;
}
false
}
}
impl<'a, D, R> RepoBuilder<'a, D, R, Done<D>>
where
D: DataInterchange + Sync,
R: RepositoryStorage<D>,
{
pub async fn commit(mut self) -> Result<RawSignedMetadataSet<D>> {
self.validate_built_metadata()?;
self.write_repo().await?;
Ok(self.build_skip_validation())
}
#[cfg(test)]
pub async fn commit_skip_validation(mut self) -> Result<RawSignedMetadataSet<D>> {
self.write_repo().await?;
Ok(self.build_skip_validation())
}
fn build_skip_validation(self) -> RawSignedMetadataSet<D> {
let mut builder = RawSignedMetadataSetBuilder::new();
if let Some(root) = self.state.staged_root {
builder = builder.root(root.raw);
}
if let Some(targets) = self.state.staged_targets {
builder = builder.targets(targets.raw);
}
if let Some(snapshot) = self.state.staged_snapshot {
builder = builder.snapshot(snapshot.raw);
}
if let Some(timestamp) = self.state.staged_timestamp {
builder = builder.timestamp(timestamp.raw);
}
builder.build()
}
fn validate_built_metadata(&self) -> Result<()> {
let mut db = if let Some(db) = self.ctx.db {
let mut db = db.clone();
if let Some(ref root) = self.state.staged_root {
db.update_root(&root.raw)?;
}
db
} else if let Some(ref root) = self.state.staged_root {
Database::from_trusted_root(&root.raw)?
} else {
return Err(Error::MissingMetadata(Role::Root));
};
if let Some(ref timestamp) = self.state.staged_timestamp {
db.update_timestamp(×tamp.raw)?;
}
if let Some(ref snapshot) = self.state.staged_snapshot {
db.update_snapshot(&snapshot.raw)?;
}
if let Some(ref targets) = self.state.staged_targets {
db.update_targets(&targets.raw)?;
}
Ok(())
}
async fn write_repo(&mut self) -> Result<()> {
let consistent_snapshot = if let Some(ref root) = self.state.staged_root {
self.ctx
.repo
.store_metadata(
&MetadataPath::from_role(&Role::Root),
&MetadataVersion::Number(root.metadata.version()),
&mut root.raw.as_bytes(),
)
.await?;
self.ctx
.repo
.store_metadata(
&MetadataPath::from_role(&Role::Root),
&MetadataVersion::None,
&mut root.raw.as_bytes(),
)
.await?;
root.metadata.consistent_snapshot()
} else if let Some(db) = self.ctx.db {
db.trusted_root().consistent_snapshot()
} else {
return Err(Error::MissingMetadata(Role::Root));
};
if let Some(ref targets) = self.state.staged_targets {
let path = MetadataPath::from_role(&Role::Targets);
self.ctx
.repo
.store_metadata(&path, &MetadataVersion::None, &mut targets.raw.as_bytes())
.await?;
if consistent_snapshot {
self.ctx
.repo
.store_metadata(
&path,
&MetadataVersion::Number(targets.metadata.version()),
&mut targets.raw.as_bytes(),
)
.await?;
}
}
if let Some(ref snapshot) = self.state.staged_snapshot {
let path = MetadataPath::from_role(&Role::Snapshot);
self.ctx
.repo
.store_metadata(&path, &MetadataVersion::None, &mut snapshot.raw.as_bytes())
.await?;
if consistent_snapshot {
self.ctx
.repo
.store_metadata(
&path,
&MetadataVersion::Number(snapshot.metadata.version()),
&mut snapshot.raw.as_bytes(),
)
.await?;
}
}
if let Some(ref timestamp) = self.state.staged_timestamp {
self.ctx
.repo
.store_metadata(
&MetadataPath::from_role(&Role::Timestamp),
&MetadataVersion::None,
&mut timestamp.raw.as_bytes(),
)
.await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::repository::RepositoryProvider;
use {
super::*,
crate::{
client::{Client, Config},
crypto::Ed25519PrivateKey,
interchange::Json,
metadata::SignedMetadata,
repository::EphemeralRepository,
},
chrono::{
offset::{TimeZone as _, Utc},
DateTime,
},
futures_executor::block_on,
futures_util::io::{AsyncReadExt, Cursor},
lazy_static::lazy_static,
matches::assert_matches,
pretty_assertions::assert_eq,
std::collections::BTreeMap,
};
lazy_static! {
static ref KEYS: Vec<Ed25519PrivateKey> = {
let keys: &[&[u8]] = &[
include_bytes!("../tests/ed25519/ed25519-1.pk8.der"),
include_bytes!("../tests/ed25519/ed25519-2.pk8.der"),
include_bytes!("../tests/ed25519/ed25519-3.pk8.der"),
include_bytes!("../tests/ed25519/ed25519-4.pk8.der"),
include_bytes!("../tests/ed25519/ed25519-5.pk8.der"),
include_bytes!("../tests/ed25519/ed25519-6.pk8.der"),
];
keys.iter()
.map(|b| Ed25519PrivateKey::from_pkcs8(b).unwrap())
.collect()
};
}
fn create_root(
version: u32,
consistent_snapshot: bool,
expires: DateTime<Utc>,
) -> SignedMetadata<Json, RootMetadata> {
let root = RootMetadataBuilder::new()
.version(version)
.consistent_snapshot(consistent_snapshot)
.expires(expires)
.root_threshold(2)
.root_key(KEYS[0].public().clone())
.root_key(KEYS[1].public().clone())
.root_key(KEYS[2].public().clone())
.targets_threshold(2)
.targets_key(KEYS[1].public().clone())
.targets_key(KEYS[2].public().clone())
.targets_key(KEYS[3].public().clone())
.snapshot_threshold(2)
.snapshot_key(KEYS[2].public().clone())
.snapshot_key(KEYS[3].public().clone())
.snapshot_key(KEYS[4].public().clone())
.timestamp_threshold(2)
.timestamp_key(KEYS[3].public().clone())
.timestamp_key(KEYS[4].public().clone())
.timestamp_key(KEYS[5].public().clone())
.build()
.unwrap();
SignedMetadataBuilder::from_metadata(&root)
.unwrap()
.sign(&KEYS[0])
.unwrap()
.sign(&KEYS[1])
.unwrap()
.sign(&KEYS[2])
.unwrap()
.build()
}
fn create_targets(
version: u32,
expires: DateTime<Utc>,
) -> SignedMetadata<Json, TargetsMetadata> {
let targets = TargetsMetadataBuilder::new()
.version(version)
.expires(expires)
.build()
.unwrap();
SignedMetadataBuilder::<Json, _>::from_metadata(&targets)
.unwrap()
.sign(&KEYS[1])
.unwrap()
.sign(&KEYS[2])
.unwrap()
.sign(&KEYS[3])
.unwrap()
.build()
}
fn create_snapshot(
version: u32,
expires: DateTime<Utc>,
targets: &SignedMetadata<Json, TargetsMetadata>,
include_length_and_hashes: bool,
) -> SignedMetadata<Json, SnapshotMetadata> {
let description = if include_length_and_hashes {
let raw_targets = targets.to_raw().unwrap();
let hashes = crypto::calculate_hashes_from_slice(
raw_targets.as_bytes(),
&[HashAlgorithm::Sha256],
)
.unwrap();
MetadataDescription::new(version, Some(raw_targets.as_bytes().len()), hashes).unwrap()
} else {
MetadataDescription::new(version, None, HashMap::new()).unwrap()
};
let snapshot = SnapshotMetadataBuilder::new()
.insert_metadata_description(MetadataPath::from_role(&Role::Targets), description)
.version(version)
.expires(expires)
.build()
.unwrap();
SignedMetadataBuilder::<Json, _>::from_metadata(&snapshot)
.unwrap()
.sign(&KEYS[2])
.unwrap()
.sign(&KEYS[3])
.unwrap()
.sign(&KEYS[4])
.unwrap()
.build()
}
fn create_timestamp(
version: u32,
expires: DateTime<Utc>,
snapshot: &SignedMetadata<Json, SnapshotMetadata>,
include_length_and_hashes: bool,
) -> SignedMetadata<Json, TimestampMetadata> {
let description = if include_length_and_hashes {
let raw_snapshot = snapshot.to_raw().unwrap();
let hashes = crypto::calculate_hashes_from_slice(
raw_snapshot.as_bytes(),
&[HashAlgorithm::Sha256],
)
.unwrap();
MetadataDescription::new(version, Some(raw_snapshot.as_bytes().len()), hashes).unwrap()
} else {
MetadataDescription::new(version, None, HashMap::new()).unwrap()
};
let timestamp = TimestampMetadataBuilder::from_metadata_description(description)
.version(version)
.expires(expires)
.build()
.unwrap();
SignedMetadataBuilder::<Json, _>::from_metadata(×tamp)
.unwrap()
.sign(&KEYS[3])
.unwrap()
.sign(&KEYS[4])
.unwrap()
.sign(&KEYS[5])
.unwrap()
.build()
}
fn assert_metadata(
metadata: &RawSignedMetadataSet<Json>,
expected_root: Option<&RawSignedMetadata<Json, RootMetadata>>,
expected_targets: Option<&RawSignedMetadata<Json, TargetsMetadata>>,
expected_snapshot: Option<&RawSignedMetadata<Json, SnapshotMetadata>>,
expected_timestamp: Option<&RawSignedMetadata<Json, TimestampMetadata>>,
) {
assert_eq!(
metadata.root().map(|m| m.parse_untrusted().unwrap()),
expected_root.map(|m| m.parse_untrusted().unwrap())
);
assert_eq!(
metadata.targets().map(|m| m.parse_untrusted().unwrap()),
expected_targets.map(|m| m.parse_untrusted().unwrap())
);
assert_eq!(
metadata.snapshot().map(|m| m.parse_untrusted().unwrap()),
expected_snapshot.map(|m| m.parse_untrusted().unwrap())
);
assert_eq!(
metadata.timestamp().map(|m| m.parse_untrusted().unwrap()),
expected_timestamp.map(|m| m.parse_untrusted().unwrap())
);
}
fn assert_repo(
repo: &EphemeralRepository<Json>,
expected_metadata: &BTreeMap<(MetadataPath, MetadataVersion), &[u8]>,
) {
let actual_metadata = repo
.metadata()
.iter()
.map(|(k, v)| (k.clone(), String::from_utf8_lossy(v).to_string()))
.collect::<BTreeMap<_, _>>();
let expected_metadata = expected_metadata
.iter()
.map(|(k, v)| (k.clone(), String::from_utf8_lossy(v).to_string()))
.collect::<BTreeMap<_, _>>();
assert_eq!(
actual_metadata.keys().collect::<Vec<_>>(),
expected_metadata.keys().collect::<Vec<_>>()
);
assert_eq!(actual_metadata, expected_metadata);
}
#[test]
fn test_stage_and_update_repo_not_consistent_snapshot() {
block_on(check_stage_and_update_repo(false));
}
#[test]
fn test_stage_and_update_repo_consistent_snapshot() {
block_on(check_stage_and_update_repo(true));
}
async fn check_stage_and_update_repo(consistent_snapshot: bool) {
let mut remote = EphemeralRepository::<Json>::new();
let expires1 = Utc.ymd(2038, 1, 1).and_hms(0, 0, 0);
let metadata1 = RepoBuilder::create(&mut remote)
.trusted_root_keys(&[&KEYS[0], &KEYS[1], &KEYS[2]])
.trusted_targets_keys(&[&KEYS[1], &KEYS[2], &KEYS[3]])
.trusted_snapshot_keys(&[&KEYS[2], &KEYS[3], &KEYS[4]])
.trusted_timestamp_keys(&[&KEYS[3], &KEYS[4], &KEYS[5]])
.stage_root_with_builder(|builder| {
builder
.expires(expires1)
.consistent_snapshot(consistent_snapshot)
.root_threshold(2)
.targets_threshold(2)
.snapshot_threshold(2)
.timestamp_threshold(2)
})
.unwrap()
.stage_targets_with_builder(|builder| builder.expires(expires1))
.unwrap()
.snapshot_includes_length(true)
.snapshot_includes_hashes(&[HashAlgorithm::Sha256])
.stage_snapshot_with_builder(|builder| builder.expires(expires1))
.unwrap()
.timestamp_includes_length(true)
.timestamp_includes_hashes(&[HashAlgorithm::Sha256])
.with_timestamp_builder(|builder| builder.expires(expires1))
.unwrap()
.commit()
.await
.unwrap();
let signed_root1 = create_root(1, consistent_snapshot, expires1);
let signed_targets1 = create_targets(1, expires1);
let signed_snapshot1 = create_snapshot(1, expires1, &signed_targets1, true);
let signed_timestamp1 = create_timestamp(1, expires1, &signed_snapshot1, true);
let raw_root1 = signed_root1.to_raw().unwrap();
let raw_targets1 = signed_targets1.to_raw().unwrap();
let raw_snapshot1 = signed_snapshot1.to_raw().unwrap();
let raw_timestamp1 = signed_timestamp1.to_raw().unwrap();
assert_metadata(
&metadata1,
Some(&raw_root1),
Some(&raw_targets1),
Some(&raw_snapshot1),
Some(&raw_timestamp1),
);
let mut expected_metadata: BTreeMap<_, _> = vec![
(
(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(1),
),
raw_root1.as_bytes(),
),
(
(MetadataPath::from_role(&Role::Root), MetadataVersion::None),
raw_root1.as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Targets),
MetadataVersion::None,
),
raw_targets1.as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::None,
),
raw_snapshot1.as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Timestamp),
MetadataVersion::None,
),
raw_timestamp1.as_bytes(),
),
]
.into_iter()
.collect();
if consistent_snapshot {
expected_metadata.extend(vec![
(
(
MetadataPath::from_role(&Role::Targets),
MetadataVersion::Number(1),
),
raw_targets1.as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::Number(1),
),
raw_snapshot1.as_bytes(),
),
]);
}
assert_repo(&remote, &expected_metadata);
let mut client = Client::with_trusted_root(
Config::default(),
metadata1.root().unwrap(),
EphemeralRepository::new(),
remote,
)
.await
.unwrap();
client.update().await.unwrap();
assert_eq!(client.trusted_root().version(), 1);
assert_eq!(client.trusted_targets().map(|m| m.version()), Some(1));
assert_eq!(client.trusted_snapshot().map(|m| m.version()), Some(1));
assert_eq!(client.trusted_timestamp().map(|m| m.version()), Some(1));
let expires2 = Utc.ymd(2038, 1, 2).and_hms(0, 0, 0);
let mut parts = client.into_parts();
let metadata2 = RepoBuilder::from_database(&mut parts.remote, &parts.database)
.trusted_root_keys(&[&KEYS[0], &KEYS[1], &KEYS[2]])
.trusted_targets_keys(&[&KEYS[1], &KEYS[2], &KEYS[3]])
.trusted_snapshot_keys(&[&KEYS[2], &KEYS[3], &KEYS[4]])
.trusted_timestamp_keys(&[&KEYS[3], &KEYS[4], &KEYS[5]])
.stage_root_with_builder(|builder| builder.expires(expires2))
.unwrap()
.stage_targets_with_builder(|builder| builder.expires(expires2))
.unwrap()
.snapshot_includes_length(false)
.snapshot_includes_hashes(&[])
.stage_snapshot_with_builder(|builder| builder.expires(expires2))
.unwrap()
.timestamp_includes_length(false)
.timestamp_includes_hashes(&[])
.with_timestamp_builder(|builder| builder.expires(expires2))
.unwrap()
.commit()
.await
.unwrap();
let signed_root2 = create_root(2, consistent_snapshot, expires2);
let signed_targets2 = create_targets(2, expires2);
let signed_snapshot2 = create_snapshot(2, expires2, &signed_targets2, false);
let signed_timestamp2 = create_timestamp(2, expires2, &signed_snapshot2, false);
let raw_root2 = signed_root2.to_raw().unwrap();
let raw_targets2 = signed_targets2.to_raw().unwrap();
let raw_snapshot2 = signed_snapshot2.to_raw().unwrap();
let raw_timestamp2 = signed_timestamp2.to_raw().unwrap();
assert_metadata(
&metadata2,
Some(&raw_root2),
Some(&raw_targets2),
Some(&raw_snapshot2),
Some(&raw_timestamp2),
);
expected_metadata.extend(vec![
(
(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(2),
),
raw_root2.as_bytes(),
),
(
(MetadataPath::from_role(&Role::Root), MetadataVersion::None),
raw_root2.as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Targets),
MetadataVersion::None,
),
raw_targets2.as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::None,
),
raw_snapshot2.as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Timestamp),
MetadataVersion::None,
),
raw_timestamp2.as_bytes(),
),
]);
if consistent_snapshot {
expected_metadata.extend(vec![
(
(
MetadataPath::from_role(&Role::Targets),
MetadataVersion::Number(2),
),
raw_targets2.as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::Number(2),
),
raw_snapshot2.as_bytes(),
),
]);
}
assert_repo(&parts.remote, &expected_metadata);
let mut client = Client::from_parts(parts);
client.update().await.unwrap();
assert_eq!(client.trusted_root().version(), 2);
assert_eq!(client.trusted_targets().map(|m| m.version()), Some(2));
assert_eq!(client.trusted_snapshot().map(|m| m.version()), Some(2));
assert_eq!(client.trusted_timestamp().map(|m| m.version()), Some(2));
}
#[test]
fn commit_does_nothing_if_nothing_changed_not_consistent_snapshot() {
block_on(commit_does_nothing_if_nothing_changed(false))
}
#[test]
fn commit_does_nothing_if_nothing_changed_consistent_snapshot() {
block_on(commit_does_nothing_if_nothing_changed(true))
}
async fn commit_does_nothing_if_nothing_changed(consistent_snapshot: bool) {
let mut repo = EphemeralRepository::<Json>::new();
let metadata1 = RepoBuilder::create(&mut repo)
.trusted_root_keys(&[&KEYS[0]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.stage_root_with_builder(|builder| builder.consistent_snapshot(consistent_snapshot))
.unwrap()
.commit()
.await
.unwrap();
let client_repo = EphemeralRepository::new();
let mut client = Client::with_trusted_root(
Config::default(),
metadata1.root().unwrap(),
client_repo,
repo,
)
.await
.unwrap();
assert!(client.update().await.unwrap());
assert_eq!(client.trusted_root().version(), 1);
let mut parts = client.into_parts();
let metadata2 = RepoBuilder::from_database(&mut parts.remote, &parts.database)
.trusted_root_keys(&[&KEYS[0]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.commit()
.await
.unwrap();
assert_metadata(&metadata2, None, None, None, None);
let mut client = Client::from_parts(parts);
assert!(!client.update().await.unwrap());
assert_eq!(client.trusted_root().version(), 1);
}
#[test]
fn root_chain_update_not_consistent() {
block_on(check_root_chain_update(false));
}
#[test]
fn root_chain_update_consistent() {
block_on(check_root_chain_update(true));
}
async fn check_root_chain_update(consistent_snapshot: bool) {
let mut repo = EphemeralRepository::<Json>::new();
let metadata1 = RepoBuilder::create(&mut repo)
.trusted_root_keys(&[&KEYS[1]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.stage_root_with_builder(|builder| builder.consistent_snapshot(consistent_snapshot))
.unwrap()
.commit()
.await
.unwrap();
let client_repo = EphemeralRepository::new();
let mut client = Client::with_trusted_root(
Config::default(),
metadata1.root().unwrap(),
client_repo,
repo,
)
.await
.unwrap();
assert!(client.update().await.unwrap());
assert_eq!(client.trusted_root().version(), 1);
assert_eq!(
client.trusted_root().root_keys().collect::<Vec<_>>(),
vec![KEYS[1].public()],
);
assert!(!client.update().await.unwrap());
assert_eq!(client.trusted_root().version(), 1);
let mut parts = client.into_parts();
let _metadata2 = RepoBuilder::from_database(&mut parts.remote, &parts.database)
.signing_root_keys(&[&KEYS[1]])
.trusted_root_keys(&[&KEYS[2]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.commit()
.await
.unwrap();
let mut client = Client::from_parts(parts);
assert!(client.update().await.unwrap());
assert_eq!(client.trusted_root().version(), 2);
assert_eq!(
client.trusted_root().consistent_snapshot(),
consistent_snapshot
);
assert_eq!(
client.trusted_root().root_keys().collect::<Vec<_>>(),
vec![KEYS[2].public()],
);
assert!(!client.update().await.unwrap());
assert_eq!(client.trusted_root().version(), 2);
let mut parts = client.into_parts();
let _metadata3 = RepoBuilder::from_database(&mut parts.remote, &parts.database)
.trusted_root_keys(&[&KEYS[2]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.stage_root()
.unwrap()
.commit()
.await
.unwrap();
let mut client = Client::from_parts(parts);
assert!(client.update().await.unwrap());
assert_eq!(client.trusted_root().version(), 3);
assert_eq!(
client.trusted_root().root_keys().collect::<Vec<_>>(),
vec![KEYS[2].public()],
);
assert!(!client.update().await.unwrap());
assert_eq!(client.trusted_root().version(), 3);
}
#[test]
fn test_from_database_root_must_be_one_after_the_last() {
block_on(async {
let mut repo = EphemeralRepository::<Json>::new();
let metadata = RepoBuilder::create(&mut repo)
.trusted_root_keys(&[&KEYS[0]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.commit()
.await
.unwrap();
let db = Database::from_trusted_metadata(&metadata).unwrap();
assert_matches!(
RepoBuilder::from_database(&mut repo, &db)
.trusted_root_keys(&[&KEYS[0]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.stage_root_with_builder(|builder| builder.version(3))
.unwrap()
.commit().await,
Err(Error::VerificationFailure(s))
if &s == "Attempted to roll back root metadata at version 1 to 3."
);
})
}
#[test]
fn test_add_target_not_consistent_snapshot() {
block_on(async move {
let mut repo = EphemeralRepository::<Json>::new();
let hash_algs = &[HashAlgorithm::Sha256, HashAlgorithm::Sha512];
let target_path = TargetPath::new("foo/bar").unwrap();
let target_file: &[u8] = b"things fade, alternatives exclude";
let metadata = RepoBuilder::create(&mut repo)
.trusted_root_keys(&[&KEYS[0]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.stage_root_with_builder(|builder| builder.consistent_snapshot(false))
.unwrap()
.file_hash_algorithms(hash_algs)
.add_target(target_path.clone(), Cursor::new(target_file))
.await
.unwrap()
.commit()
.await
.unwrap();
let mut rdr = repo.fetch_target(&target_path).await.unwrap();
let mut buf = vec![];
rdr.read_to_end(&mut buf).await.unwrap();
drop(rdr);
assert_eq!(&buf, target_file);
let mut client = Client::with_trusted_root(
Config::default(),
metadata.root().unwrap(),
EphemeralRepository::new(),
repo,
)
.await
.unwrap();
client.update().await.unwrap();
assert_eq!(
client.fetch_target_description(&target_path).await.unwrap(),
TargetDescription::from_slice(target_file, hash_algs).unwrap(),
);
let mut rdr = client.fetch_target(&target_path).await.unwrap();
let mut buf = vec![];
rdr.read_to_end(&mut buf).await.unwrap();
assert_eq!(&buf, target_file);
})
}
#[test]
fn test_add_target_consistent_snapshot() {
block_on(async move {
let mut repo = EphemeralRepository::<Json>::new();
let hash_algs = &[HashAlgorithm::Sha256, HashAlgorithm::Sha512];
let target_path = TargetPath::new("foo/bar").unwrap();
let target_file: &[u8] = b"things fade, alternatives exclude";
let metadata = RepoBuilder::create(&mut repo)
.trusted_root_keys(&[&KEYS[0]])
.trusted_targets_keys(&[&KEYS[0]])
.trusted_snapshot_keys(&[&KEYS[0]])
.trusted_timestamp_keys(&[&KEYS[0]])
.stage_root_with_builder(|builder| builder.consistent_snapshot(true))
.unwrap()
.file_hash_algorithms(hash_algs)
.add_target(target_path.clone(), Cursor::new(target_file))
.await
.unwrap()
.commit()
.await
.unwrap();
for hash_alg in hash_algs {
let hash = crypto::calculate_hash(target_file, hash_alg);
let target_path = target_path.with_hash_prefix(&hash).unwrap();
let mut rdr = repo.fetch_target(&target_path).await.unwrap();
let mut buf = vec![];
rdr.read_to_end(&mut buf).await.unwrap();
assert_eq!(&buf, target_file);
}
let mut client = Client::with_trusted_root(
Config::default(),
metadata.root().unwrap(),
EphemeralRepository::new(),
repo,
)
.await
.unwrap();
client.update().await.unwrap();
assert_eq!(
client.fetch_target_description(&target_path).await.unwrap(),
TargetDescription::from_slice(target_file, hash_algs).unwrap(),
);
let mut rdr = client.fetch_target(&target_path).await.unwrap();
let mut buf = vec![];
rdr.read_to_end(&mut buf).await.unwrap();
assert_eq!(&buf, target_file);
})
}
#[test]
fn test_do_not_require_all_keys_to_be_online() {
block_on(async {
let mut remote = EphemeralRepository::<Json>::new();
let expires1 = Utc.ymd(2038, 1, 1).and_hms(0, 0, 0);
let metadata1 = RepoBuilder::create(&mut remote)
.trusted_root_keys(&[&KEYS[0]])
.trusted_targets_keys(&[&KEYS[1]])
.trusted_snapshot_keys(&[&KEYS[2]])
.trusted_timestamp_keys(&[&KEYS[3]])
.stage_root_with_builder(|builder| {
builder.consistent_snapshot(true).expires(expires1)
})
.unwrap()
.stage_targets_with_builder(|builder| builder.expires(expires1))
.unwrap()
.stage_snapshot_with_builder(|builder| builder.expires(expires1))
.unwrap()
.with_timestamp_builder(|builder| builder.expires(expires1))
.unwrap()
.commit()
.await
.unwrap();
assert!(metadata1.root().is_some());
assert!(metadata1.timestamp().is_some());
assert!(metadata1.snapshot().is_some());
assert!(metadata1.targets().is_some());
let mut expected_metadata: BTreeMap<_, _> = vec![
(
(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(1),
),
metadata1.root().unwrap().as_bytes(),
),
(
(MetadataPath::from_role(&Role::Root), MetadataVersion::None),
metadata1.root().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Targets),
MetadataVersion::Number(1),
),
metadata1.targets().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Targets),
MetadataVersion::None,
),
metadata1.targets().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::Number(1),
),
metadata1.snapshot().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::None,
),
metadata1.snapshot().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Timestamp),
MetadataVersion::None,
),
metadata1.timestamp().unwrap().as_bytes(),
),
]
.into_iter()
.collect();
assert_repo(&remote, &expected_metadata);
let mut db = Database::from_trusted_metadata(&metadata1).unwrap();
let expires2 = Utc.ymd(2038, 1, 2).and_hms(0, 0, 0);
let metadata2 = RepoBuilder::from_database(&mut remote, &db)
.trusted_targets_keys(&[&KEYS[1]])
.trusted_snapshot_keys(&[&KEYS[2]])
.trusted_timestamp_keys(&[&KEYS[3]])
.skip_root()
.stage_targets_with_builder(|builder| builder.expires(expires2))
.unwrap()
.stage_snapshot_with_builder(|builder| builder.expires(expires2))
.unwrap()
.with_timestamp_builder(|builder| builder.expires(expires2))
.unwrap()
.commit()
.await
.unwrap();
assert!(db.update_metadata(&metadata2).unwrap());
assert!(metadata2.root().is_none());
assert!(metadata2.targets().is_some());
assert!(metadata2.snapshot().is_some());
assert!(metadata2.timestamp().is_some());
expected_metadata.extend(
vec![
(
(
MetadataPath::from_role(&Role::Targets),
MetadataVersion::Number(2),
),
metadata2.targets().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Targets),
MetadataVersion::None,
),
metadata2.targets().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::Number(2),
),
metadata2.snapshot().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::None,
),
metadata2.snapshot().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Timestamp),
MetadataVersion::None,
),
metadata2.timestamp().unwrap().as_bytes(),
),
]
.into_iter(),
);
assert_repo(&remote, &expected_metadata);
let expires3 = Utc.ymd(2038, 1, 3).and_hms(0, 0, 0);
let metadata3 = RepoBuilder::from_database(&mut remote, &db)
.trusted_snapshot_keys(&[&KEYS[2]])
.trusted_timestamp_keys(&[&KEYS[3]])
.skip_root()
.skip_targets()
.stage_snapshot_with_builder(|builder| builder.expires(expires3))
.unwrap()
.with_timestamp_builder(|builder| builder.expires(expires3))
.unwrap()
.commit()
.await
.unwrap();
assert!(db.update_metadata(&metadata3).unwrap());
assert!(metadata3.root().is_none());
assert!(metadata3.targets().is_none());
assert!(metadata3.snapshot().is_some());
assert!(metadata3.timestamp().is_some());
expected_metadata.extend(
vec![
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::Number(3),
),
metadata3.snapshot().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Snapshot),
MetadataVersion::None,
),
metadata3.snapshot().unwrap().as_bytes(),
),
(
(
MetadataPath::from_role(&Role::Timestamp),
MetadataVersion::None,
),
metadata3.timestamp().unwrap().as_bytes(),
),
]
.into_iter(),
);
assert_repo(&remote, &expected_metadata);
let expires4 = Utc.ymd(2038, 1, 4).and_hms(0, 0, 0);
let metadata4 = RepoBuilder::from_database(&mut remote, &db)
.trusted_timestamp_keys(&[&KEYS[3]])
.skip_root()
.skip_targets()
.skip_snapshot()
.with_timestamp_builder(|builder| builder.expires(expires4))
.unwrap()
.commit()
.await
.unwrap();
assert!(db.update_metadata(&metadata4).unwrap());
assert!(metadata4.root().is_none());
assert!(metadata4.targets().is_none());
assert!(metadata4.snapshot().is_none());
assert!(metadata4.timestamp().is_some());
expected_metadata.extend(
vec![(
(
MetadataPath::from_role(&Role::Timestamp),
MetadataVersion::None,
),
metadata4.timestamp().unwrap().as_bytes(),
)]
.into_iter(),
);
assert_repo(&remote, &expected_metadata);
})
}
}