use chrono::offset::Utc;
use futures_io::{AsyncRead, AsyncWrite};
use futures_util::io::copy;
use log::{error, warn};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use crate::crypto::{self, HashAlgorithm, HashValue, PublicKey};
use crate::error::Error;
use crate::interchange::DataInterchange;
use crate::metadata::{
Metadata, MetadataPath, MetadataVersion, RawSignedMetadata, Role, RootMetadata,
SnapshotMetadata, TargetDescription, TargetPath, TargetsMetadata, TimestampMetadata,
VirtualTargetPath,
};
use crate::repository::{Repository, RepositoryProvider, RepositoryStorage};
use crate::tuf::Tuf;
use crate::verify::Verified;
use crate::Result;
pub trait PathTranslator {
fn real_to_virtual(&self, path: &TargetPath) -> Result<VirtualTargetPath>;
fn virtual_to_real(&self, path: &VirtualTargetPath) -> Result<TargetPath>;
}
#[derive(Clone, Debug, Default)]
pub struct DefaultTranslator;
impl DefaultTranslator {
pub fn new() -> Self {
DefaultTranslator
}
}
impl PathTranslator for DefaultTranslator {
fn real_to_virtual(&self, path: &TargetPath) -> Result<VirtualTargetPath> {
VirtualTargetPath::new(path.value().into())
}
fn virtual_to_real(&self, path: &VirtualTargetPath) -> Result<TargetPath> {
TargetPath::new(path.value().into())
}
}
#[derive(Debug)]
pub struct Client<D, L, R, T>
where
D: DataInterchange + Sync,
L: RepositoryProvider<D> + RepositoryStorage<D>,
R: RepositoryProvider<D>,
T: PathTranslator,
{
tuf: Tuf<D>,
config: Config<T>,
local: Repository<L, D>,
remote: Repository<R, D>,
}
impl<D, L, R, T> Client<D, L, R, T>
where
D: DataInterchange + Sync,
L: RepositoryProvider<D> + RepositoryStorage<D>,
R: RepositoryProvider<D>,
T: PathTranslator,
{
pub async fn with_trusted_local(config: Config<T>, local: L, remote: R) -> Result<Self> {
let (local, remote) = (Repository::new(local), Repository::new(remote));
let root_path = MetadataPath::from_role(&Role::Root);
let root_version = MetadataVersion::Number(1);
let raw_root: RawSignedMetadata<_, RootMetadata> = local
.fetch_metadata(&root_path, &root_version, config.max_root_length, None)
.await?;
let tuf = Tuf::from_trusted_root(&raw_root)?;
Self::new(config, tuf, local, remote).await
}
pub async fn with_trusted_root(
config: Config<T>,
trusted_root: &RawSignedMetadata<D, RootMetadata>,
local: L,
remote: R,
) -> Result<Self> {
let (local, remote) = (Repository::new(local), Repository::new(remote));
let tuf = Tuf::from_trusted_root(trusted_root)?;
Self::new(config, tuf, local, remote).await
}
pub async fn with_trusted_root_keys<'a, I>(
config: Config<T>,
root_version: &MetadataVersion,
root_threshold: u32,
trusted_root_keys: I,
local: L,
remote: R,
) -> Result<Self>
where
I: IntoIterator<Item = &'a PublicKey>,
{
let (local, remote) = (Repository::new(local), Repository::new(remote));
let root_path = MetadataPath::from_role(&Role::Root);
let (fetched, raw_root) = fetch_metadata_from_local_or_else_remote(
&root_path,
root_version,
config.max_root_length,
None,
&local,
&remote,
)
.await?;
let tuf = Tuf::from_root_with_trusted_keys(&raw_root, root_threshold, trusted_root_keys)?;
let root_version = MetadataVersion::Number(tuf.trusted_root().version());
if fetched {
local
.store_metadata(&root_path, &root_version, &raw_root)
.await?;
}
Self::new(config, tuf, local, remote).await
}
async fn new(
config: Config<T>,
mut tuf: Tuf<D>,
local: Repository<L, D>,
remote: Repository<R, D>,
) -> Result<Self> {
let res = async {
let _r = Self::update_root_with_repos(&config, &mut tuf, None, &local).await?;
let _ts = Self::update_timestamp_with_repos(&config, &mut tuf, None, &local).await?;
let _sn = Self::update_snapshot_with_repos(&mut tuf, None, &local, false).await?;
let _ta = Self::update_targets_with_repos(&mut tuf, None, &local, false).await?;
Ok(())
}
.await;
match res {
Ok(()) | Err(Error::NotFound) => {}
Err(err) => {
warn!("error loading local metadata: : {}", err);
}
}
Ok(Client {
tuf,
config,
local,
remote,
})
}
pub async fn update(&mut self) -> Result<bool> {
let r = self.update_root().await?;
let ts = self.update_timestamp().await?;
let sn = self.update_snapshot().await?;
let ta = self.update_targets().await?;
Ok(r || ts || sn || ta)
}
pub fn root_version(&self) -> u32 {
self.tuf.trusted_root().version()
}
pub fn timestamp_version(&self) -> Option<u32> {
Some(self.tuf.trusted_timestamp()?.version())
}
pub fn snapshot_version(&self) -> Option<u32> {
Some(self.tuf.trusted_snapshot()?.version())
}
pub fn targets_version(&self) -> Option<u32> {
Some(self.tuf.trusted_targets()?.version())
}
pub fn delegations_version(&self, role: &MetadataPath) -> Option<u32> {
Some(self.tuf.trusted_delegations().get(role)?.version())
}
pub fn trusted_root(&self) -> &Verified<RootMetadata> {
self.tuf.trusted_root()
}
pub fn trusted_timestamp(&self) -> Option<&Verified<TimestampMetadata>> {
self.tuf.trusted_timestamp()
}
pub fn trusted_snapshot(&self) -> Option<&Verified<SnapshotMetadata>> {
self.tuf.trusted_snapshot()
}
pub fn trusted_targets(&self) -> Option<&Verified<TargetsMetadata>> {
self.tuf.trusted_targets()
}
pub fn trusted_delegations(&self) -> &HashMap<MetadataPath, Verified<TargetsMetadata>> {
self.tuf.trusted_delegations()
}
pub async fn update_root(&mut self) -> Result<bool> {
Self::update_root_with_repos(&self.config, &mut self.tuf, Some(&self.local), &self.remote)
.await
}
async fn update_root_with_repos<Remote>(
config: &Config<T>,
tuf: &mut Tuf<D>,
local: Option<&Repository<L, D>>,
remote: &Repository<Remote, D>,
) -> Result<bool>
where
Remote: RepositoryProvider<D>,
{
let root_path = MetadataPath::from_role(&Role::Root);
let mut updated = false;
loop {
let next_version = MetadataVersion::Number(tuf.trusted_root().version() + 1);
let res = remote
.fetch_metadata(&root_path, &next_version, config.max_root_length, None)
.await;
let raw_signed_root = match res {
Ok(raw_signed_root) => raw_signed_root,
Err(Error::NotFound) => {
break;
}
Err(err) => {
return Err(err);
}
};
updated = true;
tuf.update_root(&raw_signed_root)?;
if let Some(local) = local {
local
.store_metadata(&root_path, &MetadataVersion::None, &raw_signed_root)
.await?;
local
.store_metadata(&root_path, &next_version, &raw_signed_root)
.await?;
}
}
if tuf.trusted_root().expires() <= &Utc::now() {
error!("Root metadata expired, potential freeze attack");
return Err(Error::ExpiredMetadata(Role::Root));
}
Ok(updated)
}
async fn update_timestamp(&mut self) -> Result<bool> {
Self::update_timestamp_with_repos(
&self.config,
&mut self.tuf,
Some(&self.local),
&self.remote,
)
.await
}
async fn update_timestamp_with_repos<Remote>(
config: &Config<T>,
tuf: &mut Tuf<D>,
local: Option<&Repository<L, D>>,
remote: &Repository<Remote, D>,
) -> Result<bool>
where
Remote: RepositoryProvider<D>,
{
let timestamp_path = MetadataPath::from_role(&Role::Timestamp);
let raw_signed_timestamp = remote
.fetch_metadata(
×tamp_path,
&MetadataVersion::None,
config.max_timestamp_length,
None,
)
.await?;
if tuf.update_timestamp(&raw_signed_timestamp)?.is_some() {
if let Some(local) = local {
local
.store_metadata(
×tamp_path,
&MetadataVersion::None,
&raw_signed_timestamp,
)
.await?;
}
Ok(true)
} else {
Ok(false)
}
}
async fn update_snapshot(&mut self) -> Result<bool> {
let consistent_snapshot = self.tuf.trusted_root().consistent_snapshot();
Self::update_snapshot_with_repos(
&mut self.tuf,
Some(&self.local),
&self.remote,
consistent_snapshot,
)
.await
}
async fn update_snapshot_with_repos<Remote>(
tuf: &mut Tuf<D>,
local: Option<&Repository<L, D>>,
remote: &Repository<Remote, D>,
consistent_snapshots: bool,
) -> Result<bool>
where
Remote: RepositoryProvider<D>,
{
let snapshot_description = match tuf.trusted_timestamp() {
Some(ts) => Ok(ts.snapshot()),
None => Err(Error::MissingMetadata(Role::Timestamp)),
}?
.clone();
if snapshot_description.version()
<= tuf.trusted_snapshot().map(|s| s.version()).unwrap_or(0)
{
return Ok(false);
}
let (alg, value) = crypto::hash_preference(snapshot_description.hashes())?;
let version = if consistent_snapshots {
MetadataVersion::Number(snapshot_description.version())
} else {
MetadataVersion::None
};
let snapshot_path = MetadataPath::from_role(&Role::Snapshot);
let snapshot_length = Some(snapshot_description.length());
let raw_signed_snapshot = remote
.fetch_metadata(
&snapshot_path,
&version,
snapshot_length,
Some((alg, value.clone())),
)
.await?;
if tuf.update_snapshot(&raw_signed_snapshot)? {
if let Some(local) = local {
local
.store_metadata(&snapshot_path, &MetadataVersion::None, &raw_signed_snapshot)
.await?;
}
Ok(true)
} else {
Ok(false)
}
}
async fn update_targets(&mut self) -> Result<bool> {
let consistent_snapshot = self.tuf.trusted_root().consistent_snapshot();
Self::update_targets_with_repos(
&mut self.tuf,
Some(&self.local),
&self.remote,
consistent_snapshot,
)
.await
}
async fn update_targets_with_repos<Remote>(
tuf: &mut Tuf<D>,
local: Option<&Repository<L, D>>,
remote: &Repository<Remote, D>,
consistent_snapshot: bool,
) -> Result<bool>
where
Remote: RepositoryProvider<D>,
{
let targets_description = match tuf.trusted_snapshot() {
Some(sn) => match sn.meta().get(&MetadataPath::from_role(&Role::Targets)) {
Some(d) => Ok(d),
None => Err(Error::VerificationFailure(
"Snapshot metadata did not contain a description of the \
current targets metadata."
.into(),
)),
},
None => Err(Error::MissingMetadata(Role::Snapshot)),
}?
.clone();
if targets_description.version() <= tuf.trusted_targets().map(|t| t.version()).unwrap_or(0)
{
return Ok(false);
}
let (alg, value) = crypto::hash_preference(targets_description.hashes())?;
let version = if consistent_snapshot {
MetadataVersion::Number(targets_description.version())
} else {
MetadataVersion::None
};
let targets_path = MetadataPath::from_role(&Role::Targets);
let targets_length = Some(targets_description.length());
let raw_signed_targets = remote
.fetch_metadata(
&targets_path,
&version,
targets_length,
Some((alg, value.clone())),
)
.await?;
if tuf.update_targets(&raw_signed_targets)? {
if let Some(local) = local {
local
.store_metadata(&targets_path, &MetadataVersion::None, &raw_signed_targets)
.await?;
}
Ok(true)
} else {
Ok(false)
}
}
pub async fn fetch_target<'a>(&'a mut self, target: &'a TargetPath) -> Result<()> {
let mut read = self._fetch_target(target).await?;
self.local.store_target(&mut read, target).await
}
pub async fn fetch_target_to_writer<'a, W>(
&'a mut self,
target: &'a TargetPath,
mut write: W,
) -> Result<()>
where
W: AsyncWrite + Send + Unpin,
{
let read = self._fetch_target(&target).await?;
copy(read, &mut write).await?;
Ok(())
}
pub async fn fetch_target_description<'a>(
&'a mut self,
target: &'a TargetPath,
) -> Result<TargetDescription> {
let virt = self.config.path_translator.real_to_virtual(target)?;
let snapshot = self
.tuf
.trusted_snapshot()
.ok_or_else(|| Error::MissingMetadata(Role::Snapshot))?
.clone();
let (_, target_description) = self
.lookup_target_description(false, 0, &virt, &snapshot, None)
.await;
target_description
}
async fn _fetch_target<'a>(
&'a mut self,
target: &'a TargetPath,
) -> Result<impl AsyncRead + Send + Unpin> {
let target_description = self.fetch_target_description(target).await?;
if self.tuf.trusted_root().consistent_snapshot() {
let (_, value) = crypto::hash_preference(target_description.hashes())?;
let target = target.with_hash_prefix(value)?;
self.remote.fetch_target(&target, &target_description).await
} else {
self.remote.fetch_target(target, &target_description).await
}
}
async fn lookup_target_description<'a>(
&'a mut self,
default_terminate: bool,
current_depth: u32,
target: &'a VirtualTargetPath,
snapshot: &'a SnapshotMetadata,
targets: Option<(&'a Verified<TargetsMetadata>, MetadataPath)>,
) -> (bool, Result<TargetDescription>) {
if current_depth > self.config.max_delegation_depth {
warn!(
"Walking the delegation graph would have exceeded the configured max depth: {}",
self.config.max_delegation_depth
);
return (default_terminate, Err(Error::NotFound));
}
let (targets, targets_role) = match targets {
Some((t, role)) => (t.clone(), role),
None => match self.tuf.trusted_targets() {
Some(t) => (t.clone(), MetadataPath::from_role(&Role::Targets)),
None => {
return (
default_terminate,
Err(Error::MissingMetadata(Role::Targets)),
);
}
},
};
if let Some(t) = targets.targets().get(target) {
return (default_terminate, Ok(t.clone()));
}
let delegations = match targets.delegations() {
Some(d) => d,
None => return (default_terminate, Err(Error::NotFound)),
};
for delegation in delegations.roles().iter() {
if !delegation.paths().iter().any(|p| target.is_child(p)) {
if delegation.terminating() {
return (true, Err(Error::NotFound));
} else {
continue;
}
}
let role_meta = match snapshot.meta().get(delegation.role()) {
Some(m) => m,
None if !delegation.terminating() => continue,
None => return (true, Err(Error::NotFound)),
};
let (alg, value) = match crypto::hash_preference(role_meta.hashes()) {
Ok(h) => h,
Err(e) => return (delegation.terminating(), Err(e)),
};
let version = if self.tuf.trusted_root().consistent_snapshot() {
MetadataVersion::Number(role_meta.version())
} else {
MetadataVersion::None
};
let role_length = Some(role_meta.length());
let raw_signed_meta = self
.local
.fetch_metadata(
delegation.role(),
&MetadataVersion::None,
role_length,
Some((alg, value.clone())),
)
.await;
let raw_signed_meta = match raw_signed_meta {
Ok(m) => m,
Err(_) => {
match self
.remote
.fetch_metadata(
delegation.role(),
&version,
role_length,
Some((alg, value.clone())),
)
.await
{
Ok(m) => m,
Err(ref e) if !delegation.terminating() => {
warn!("Failed to fetch metadata {:?}: {:?}", delegation.role(), e);
continue;
}
Err(e) => {
warn!("Failed to fetch metadata {:?}: {:?}", delegation.role(), e);
return (true, Err(e));
}
}
}
};
match self
.tuf
.update_delegation(&targets_role, delegation.role(), &raw_signed_meta)
{
Ok(_) => {
match self
.local
.store_metadata(delegation.role(), &MetadataVersion::None, &raw_signed_meta)
.await
{
Ok(_) => (),
Err(e) => {
warn!(
"Error storing metadata {:?} locally: {:?}",
delegation.role(),
e
)
}
}
let meta = self
.tuf
.trusted_delegations()
.get(delegation.role())
.unwrap()
.clone();
let f: Pin<Box<dyn Future<Output = _>>> =
Box::pin(self.lookup_target_description(
delegation.terminating(),
current_depth + 1,
target,
snapshot,
Some((&meta, delegation.role().clone())),
));
let (term, res) = f.await;
if term && res.is_err() {
return (true, res);
}
}
Err(_) if !delegation.terminating() => continue,
Err(e) => return (true, Err(e)),
};
}
(default_terminate, Err(Error::NotFound))
}
}
async fn fetch_metadata_from_local_or_else_remote<'a, D, L, R, M>(
path: &'a MetadataPath,
version: &'a MetadataVersion,
max_length: Option<usize>,
hash_data: Option<(&'static HashAlgorithm, HashValue)>,
local: &'a Repository<L, D>,
remote: &'a Repository<R, D>,
) -> Result<(bool, RawSignedMetadata<D, M>)>
where
D: DataInterchange + Sync,
L: RepositoryProvider<D> + RepositoryStorage<D>,
R: RepositoryProvider<D>,
M: Metadata + 'static,
{
match local
.fetch_metadata(path, version, max_length, hash_data.clone())
.await
{
Ok(raw_meta) => Ok((false, raw_meta)),
Err(Error::NotFound) => {
let raw_meta = remote
.fetch_metadata(path, version, max_length, hash_data)
.await?;
Ok((true, raw_meta))
}
Err(err) => Err(err),
}
}
#[derive(Clone, Debug)]
pub struct Config<T>
where
T: PathTranslator,
{
max_root_length: Option<usize>,
max_timestamp_length: Option<usize>,
max_delegation_depth: u32,
path_translator: T,
}
impl Config<DefaultTranslator> {
pub fn build() -> ConfigBuilder<DefaultTranslator> {
ConfigBuilder::default()
}
}
impl<T> Config<T>
where
T: PathTranslator,
{
pub fn max_root_length(&self) -> &Option<usize> {
&self.max_root_length
}
pub fn max_timestamp_length(&self) -> &Option<usize> {
&self.max_timestamp_length
}
pub fn max_delegation_depth(&self) -> u32 {
self.max_delegation_depth
}
pub fn path_translator(&self) -> &T {
&self.path_translator
}
}
impl Default for Config<DefaultTranslator> {
fn default() -> Self {
Config {
max_root_length: Some(1024 * 1024),
max_timestamp_length: Some(32 * 1024),
max_delegation_depth: 8,
path_translator: DefaultTranslator::new(),
}
}
}
#[derive(Debug, PartialEq)]
pub struct ConfigBuilder<T>
where
T: PathTranslator,
{
max_root_length: Option<usize>,
max_timestamp_length: Option<usize>,
max_delegation_depth: u32,
path_translator: T,
}
impl<T> ConfigBuilder<T>
where
T: PathTranslator,
{
pub fn finish(self) -> Result<Config<T>> {
Ok(Config {
max_root_length: self.max_root_length,
max_timestamp_length: self.max_timestamp_length,
max_delegation_depth: self.max_delegation_depth,
path_translator: self.path_translator,
})
}
pub fn max_root_length(mut self, max: Option<usize>) -> Self {
self.max_root_length = max;
self
}
pub fn max_timestamp_length(mut self, max: Option<usize>) -> Self {
self.max_timestamp_length = max;
self
}
pub fn max_delegation_depth(mut self, max: u32) -> Self {
self.max_delegation_depth = max;
self
}
pub fn path_translator<TT>(self, path_translator: TT) -> ConfigBuilder<TT>
where
TT: PathTranslator,
{
ConfigBuilder {
max_root_length: self.max_root_length,
max_timestamp_length: self.max_timestamp_length,
max_delegation_depth: self.max_delegation_depth,
path_translator,
}
}
}
impl Default for ConfigBuilder<DefaultTranslator> {
fn default() -> ConfigBuilder<DefaultTranslator> {
let cfg = Config::default();
ConfigBuilder {
max_root_length: cfg.max_root_length,
max_timestamp_length: cfg.max_timestamp_length,
max_delegation_depth: cfg.max_delegation_depth,
path_translator: cfg.path_translator,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::crypto::{Ed25519PrivateKey, HashAlgorithm, PrivateKey};
use crate::interchange::Json;
use crate::metadata::{MetadataPath, MetadataVersion};
use crate::repo_builder::RepoBuilder;
use crate::repository::{EphemeralRepository, ErrorRepository, Track, TrackRepository};
use chrono::prelude::*;
use futures_executor::block_on;
use lazy_static::lazy_static;
use maplit::hashmap;
use matches::assert_matches;
use serde_json::json;
use std::iter::once;
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()
};
}
enum ConstructorMode {
WithTrustedLocal,
WithTrustedRoot,
WithTrustedRootKeys,
}
#[test]
fn client_constructors_err_with_not_found() {
block_on(async {
let local = EphemeralRepository::<Json>::new();
let remote = EphemeralRepository::<Json>::new();
let private_key =
Ed25519PrivateKey::from_pkcs8(&Ed25519PrivateKey::pkcs8().unwrap()).unwrap();
let public_key = private_key.public().clone();
assert_matches!(
Client::with_trusted_local(Config::default(), &local, &remote).await,
Err(Error::NotFound)
);
assert_matches!(
Client::with_trusted_root_keys(
Config::default(),
&MetadataVersion::Number(1),
1,
once(&public_key),
&local,
&remote,
)
.await,
Err(Error::NotFound)
);
})
}
#[test]
fn client_constructors_err_with_invalid_keys() {
block_on(async {
let remote = EphemeralRepository::new();
let root_version = 1;
let good_private_key = &KEYS[0];
let bad_private_key = &KEYS[1];
let _ = RepoBuilder::<_, Json>::new(&remote)
.root_keys(vec![good_private_key])
.with_root_builder(|bld| {
bld.version(root_version)
.expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0))
.root_key(good_private_key.public().clone())
.snapshot_key(good_private_key.public().clone())
.targets_key(good_private_key.public().clone())
.timestamp_key(good_private_key.public().clone())
})
.commit()
.await
.unwrap();
assert_matches!(
Client::with_trusted_root_keys(
Config::default(),
&MetadataVersion::Number(root_version),
1,
once(bad_private_key.public()),
EphemeralRepository::new(),
&remote,
)
.await,
Err(Error::VerificationFailure(_))
);
})
}
#[test]
fn with_trusted_local_loads_metadata_from_local_repo() {
block_on(constructors_load_metadata_from_local_repo(
ConstructorMode::WithTrustedLocal,
))
}
#[test]
fn with_trusted_root_loads_metadata_from_local_repo() {
block_on(constructors_load_metadata_from_local_repo(
ConstructorMode::WithTrustedRoot,
))
}
#[test]
fn with_trusted_root_keys_loads_metadata_from_local_repo() {
block_on(constructors_load_metadata_from_local_repo(
ConstructorMode::WithTrustedRootKeys,
))
}
async fn constructors_load_metadata_from_local_repo(constructor_mode: ConstructorMode) {
let local = EphemeralRepository::<Json>::new();
let metadata1 = RepoBuilder::new(&local)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.version(1)
.consistent_snapshot(true)
.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(1)
.commit()
.await
.unwrap();
let remote = EphemeralRepository::<Json>::new();
let metadata2 = RepoBuilder::new(&remote)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.version(2)
.consistent_snapshot(true)
.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(2)
.snapshot_version(2)
.timestamp_version(2)
.commit()
.await
.unwrap();
let track_local = TrackRepository::new(&local);
let track_remote = TrackRepository::new(&remote);
let mut client = match constructor_mode {
ConstructorMode::WithTrustedLocal => {
Client::with_trusted_local(Config::default(), &track_local, &track_remote)
.await
.unwrap()
}
ConstructorMode::WithTrustedRoot => Client::with_trusted_root(
Config::default(),
&metadata1.root,
&track_local,
&track_remote,
)
.await
.unwrap(),
ConstructorMode::WithTrustedRootKeys => Client::with_trusted_root_keys(
Config::default(),
&MetadataVersion::Number(1),
1,
once(&KEYS[0].public().clone()),
&track_local,
&track_remote,
)
.await
.unwrap(),
};
assert_eq!(client.tuf.trusted_root().version(), 1);
assert_eq!(track_remote.take_tracks(), vec![]);
match constructor_mode {
ConstructorMode::WithTrustedLocal => {
assert_eq!(
track_local.take_tracks(),
vec![
Track::fetch_meta_found(&MetadataVersion::Number(1), &metadata1.root),
Track::FetchErr(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(2)
),
Track::fetch_meta_found(
&MetadataVersion::None,
&metadata1.timestamp.as_ref().unwrap()
),
Track::fetch_meta_found(
&MetadataVersion::None,
&metadata1.snapshot.as_ref().unwrap()
),
Track::fetch_meta_found(
&MetadataVersion::None,
metadata1.targets.as_ref().unwrap()
),
],
);
}
ConstructorMode::WithTrustedRoot => {
assert_eq!(
track_local.take_tracks(),
vec![
Track::FetchErr(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(2)
),
Track::fetch_meta_found(
&MetadataVersion::None,
&metadata1.timestamp.as_ref().unwrap()
),
Track::fetch_meta_found(
&MetadataVersion::None,
&metadata1.snapshot.as_ref().unwrap()
),
Track::fetch_meta_found(
&MetadataVersion::None,
metadata1.targets.as_ref().unwrap()
),
],
);
}
ConstructorMode::WithTrustedRootKeys => {
assert_eq!(
track_local.take_tracks(),
vec![
Track::fetch_meta_found(&MetadataVersion::Number(1), &metadata1.root),
Track::FetchErr(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(2)
),
Track::fetch_meta_found(
&MetadataVersion::None,
&metadata1.timestamp.as_ref().unwrap()
),
Track::fetch_meta_found(
&MetadataVersion::None,
&metadata1.snapshot.as_ref().unwrap()
),
Track::fetch_meta_found(
&MetadataVersion::None,
metadata1.targets.as_ref().unwrap()
),
],
);
}
};
assert_matches!(client.update().await, Ok(true));
assert_eq!(client.tuf.trusted_root().version(), 2);
assert_eq!(
track_remote.take_tracks(),
vec![
Track::fetch_meta_found(&MetadataVersion::Number(2), &metadata2.root),
Track::FetchErr(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(3)
),
Track::fetch_meta_found(
&MetadataVersion::None,
&metadata2.timestamp.as_ref().unwrap()
),
Track::fetch_meta_found(
&MetadataVersion::Number(2),
&metadata2.snapshot.as_ref().unwrap()
),
Track::fetch_meta_found(
&MetadataVersion::Number(2),
metadata2.targets.as_ref().unwrap()
),
],
);
assert_eq!(
track_local.take_tracks(),
vec![
Track::store_meta(&MetadataVersion::None, &metadata2.root),
Track::store_meta(&MetadataVersion::Number(2), &metadata2.root),
Track::store_meta(
&MetadataVersion::None,
metadata2.timestamp.as_ref().unwrap()
),
Track::store_meta(&MetadataVersion::None, metadata2.snapshot.as_ref().unwrap()),
Track::store_meta(&MetadataVersion::None, metadata2.targets.as_ref().unwrap()),
],
);
assert_matches!(client.update().await, Ok(false));
assert_eq!(client.tuf.trusted_root().version(), 2);
assert_eq!(
track_remote.take_tracks(),
vec![
Track::FetchErr(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(3)
),
Track::fetch_meta_found(
&MetadataVersion::None,
metadata2.timestamp.as_ref().unwrap(),
),
]
);
assert_eq!(track_local.take_tracks(), vec![]);
}
#[test]
fn constructor_succeeds_with_missing_metadata() {
block_on(async {
let local = EphemeralRepository::<Json>::new();
let remote = EphemeralRepository::<Json>::new();
let metadata1 = RepoBuilder::new(&local)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.version(1)
.consistent_snapshot(true)
.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())
.timestamp_key(KEYS[0].public().clone())
})
.commit()
.await
.unwrap();
let track_local = TrackRepository::new(&local);
let track_remote = TrackRepository::new(&remote);
let mut client = Client::with_trusted_root(
Config::default(),
&metadata1.root,
&track_local,
&track_remote,
)
.await
.unwrap();
assert_eq!(client.tuf.trusted_root().version(), 1);
assert_eq!(track_remote.take_tracks(), vec![]);
assert_eq!(
track_local.take_tracks(),
vec![
Track::FetchErr(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(2)
),
Track::FetchErr(
MetadataPath::from_role(&Role::Timestamp),
MetadataVersion::None
)
],
);
let _metadata2 = RepoBuilder::new(&remote)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.version(2)
.consistent_snapshot(true)
.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(1)
.snapshot_version(1)
.timestamp_version(1)
.commit()
.await
.unwrap();
assert_matches!(client.update().await, Ok(true));
assert_eq!(client.tuf.trusted_root().version(), 2);
})
}
#[test]
fn constructor_succeeds_with_expired_metadata() {
block_on(async {
let local = EphemeralRepository::<Json>::new();
let remote = EphemeralRepository::<Json>::new();
let metadata1 = RepoBuilder::new(&local)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.version(1)
.consistent_snapshot(true)
.expires(Utc.ymd(1970, 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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(1)
.commit()
.await
.unwrap();
let metadata2 = RepoBuilder::new(&local)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.version(2)
.consistent_snapshot(true)
.expires(Utc.ymd(1970, 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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(2)
.snapshot_version(2)
.timestamp_version(2)
.commit()
.await
.unwrap();
let track_local = TrackRepository::new(&local);
let track_remote = TrackRepository::new(&remote);
let mut client = Client::with_trusted_root(
Config::default(),
&metadata1.root,
&track_local,
&track_remote,
)
.await
.unwrap();
assert_eq!(client.tuf.trusted_root().version(), 2);
assert_eq!(track_remote.take_tracks(), vec![]);
assert_eq!(
track_local.take_tracks(),
vec![
Track::fetch_meta_found(&MetadataVersion::Number(2), &metadata2.root),
Track::FetchErr(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(3)
)
],
);
let _metadata3 = RepoBuilder::new(&remote)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.version(3)
.consistent_snapshot(true)
.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(3)
.snapshot_version(3)
.timestamp_version(3)
.commit()
.await
.unwrap();
assert_matches!(client.update().await, Ok(true));
assert_eq!(client.tuf.trusted_root().version(), 3);
})
}
#[test]
fn constructor_succeeds_with_malformed_metadata() {
block_on(async {
let local = EphemeralRepository::<Json>::new();
let junk_timestamp = "junk timestamp";
local
.store_metadata(
&MetadataPath::from_role(&Role::Timestamp),
&MetadataVersion::None,
&mut junk_timestamp.as_bytes(),
)
.await
.unwrap();
let remote = EphemeralRepository::<Json>::new();
let metadata1 = RepoBuilder::new(&remote)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(1)
.commit()
.await
.unwrap();
let track_local = TrackRepository::new(&local);
let track_remote = TrackRepository::new(&remote);
let mut client = Client::with_trusted_root(
Config::default(),
&metadata1.root,
&track_local,
&track_remote,
)
.await
.unwrap();
assert_eq!(client.tuf.trusted_root().version(), 1);
assert_eq!(track_remote.take_tracks(), vec![]);
assert_eq!(
track_local.take_tracks(),
vec![
Track::FetchErr(
MetadataPath::from_role(&Role::Root),
MetadataVersion::Number(2)
),
Track::FetchFound {
path: MetadataPath::from_role(&Role::Timestamp),
version: MetadataVersion::None,
metadata: junk_timestamp.into(),
},
],
);
assert_matches!(client.update().await, Ok(true));
})
}
#[test]
fn root_chain_update_consistent_snapshot_false() {
block_on(root_chain_update(false))
}
#[test]
fn root_chain_update_consistent_snapshot_true() {
block_on(root_chain_update(true))
}
async fn root_chain_update(consistent_snapshot: bool) {
let repo = EphemeralRepository::<Json>::new();
let metadata1 = RepoBuilder::new(&repo)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.snapshot_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.timestamp_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.with_root_builder(|bld| {
bld.version(1)
.consistent_snapshot(consistent_snapshot)
.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(1)
.commit()
.await
.unwrap();
let root_path = MetadataPath::from_role(&Role::Root);
let timestamp_path = MetadataPath::from_role(&Role::Timestamp);
let targets_version;
let snapshot_version;
if consistent_snapshot {
targets_version = MetadataVersion::Number(1);
snapshot_version = MetadataVersion::Number(1);
} else {
targets_version = MetadataVersion::None;
snapshot_version = MetadataVersion::None;
};
let track_local = TrackRepository::new(EphemeralRepository::new());
let track_remote = TrackRepository::new(&repo);
let mut client = Client::with_trusted_root_keys(
Config::default(),
&MetadataVersion::Number(1),
1,
once(&KEYS[0].public().clone()),
&track_local,
&track_remote,
)
.await
.unwrap();
assert_eq!(
track_remote.take_tracks(),
vec![Track::fetch_found(
&root_path,
&MetadataVersion::Number(1),
metadata1.root.as_bytes()
),]
);
assert_eq!(
track_local.take_tracks(),
vec![
Track::FetchErr(root_path.clone(), MetadataVersion::Number(1)),
Track::store_meta(&MetadataVersion::Number(1), &metadata1.root),
Track::FetchErr(root_path.clone(), MetadataVersion::Number(2)),
Track::FetchErr(timestamp_path.clone(), MetadataVersion::None),
]
);
assert_matches!(client.update().await, Ok(true));
assert_eq!(client.tuf.trusted_root().version(), 1);
assert_eq!(
track_remote.take_tracks(),
vec![
Track::FetchErr(root_path.clone(), MetadataVersion::Number(2)),
Track::fetch_meta_found(
&MetadataVersion::None,
&metadata1.timestamp.as_ref().unwrap()
),
Track::fetch_meta_found(&snapshot_version, &metadata1.snapshot.as_ref().unwrap()),
Track::fetch_meta_found(&targets_version, metadata1.targets.as_ref().unwrap()),
]
);
assert_eq!(
track_local.take_tracks(),
vec![
Track::store_meta(
&MetadataVersion::None,
metadata1.timestamp.as_ref().unwrap()
),
Track::store_meta(&MetadataVersion::None, metadata1.snapshot.as_ref().unwrap()),
Track::store_meta(&MetadataVersion::None, metadata1.targets.as_ref().unwrap()),
],
);
assert_matches!(client.update().await, Ok(false));
assert_eq!(client.tuf.trusted_root().version(), 1);
assert_eq!(
track_remote.take_tracks(),
vec![
Track::FetchErr(root_path.clone(), MetadataVersion::Number(2)),
Track::fetch_meta_found(
&MetadataVersion::None,
metadata1.timestamp.as_ref().unwrap(),
),
]
);
assert_eq!(track_local.take_tracks(), vec![]);
let metadata2 = RepoBuilder::new(&repo)
.root_keys(vec![&KEYS[0], &KEYS[1]])
.targets_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.snapshot_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.timestamp_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.with_root_builder(|bld| {
bld.version(2)
.consistent_snapshot(consistent_snapshot)
.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())
.timestamp_key(KEYS[1].public().clone())
})
.commit()
.await
.unwrap();
let metadata3 = RepoBuilder::new(&repo)
.root_keys(vec![&KEYS[1], &KEYS[2]])
.targets_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.snapshot_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.timestamp_keys(vec![&KEYS[0], &KEYS[1], &KEYS[2]])
.with_root_builder(|bld| {
bld.version(3)
.consistent_snapshot(consistent_snapshot)
.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())
.timestamp_key(KEYS[2].public().clone())
})
.commit()
.await
.unwrap();
assert_matches!(client.update().await, Ok(true));
assert_eq!(client.tuf.trusted_root().version(), 3);
assert_eq!(
track_remote.take_tracks(),
vec![
Track::fetch_meta_found(&MetadataVersion::Number(2), &metadata2.root,),
Track::fetch_meta_found(&MetadataVersion::Number(3), &metadata3.root,),
Track::FetchErr(root_path.clone(), MetadataVersion::Number(4)),
Track::fetch_meta_found(
&MetadataVersion::None,
metadata1.timestamp.as_ref().unwrap(),
),
Track::fetch_meta_found(&snapshot_version, metadata1.snapshot.as_ref().unwrap(),),
Track::fetch_meta_found(&targets_version, metadata1.targets.as_ref().unwrap(),),
]
);
assert_eq!(
track_local.take_tracks(),
vec![
Track::store_meta(&MetadataVersion::None, &metadata2.root,),
Track::store_meta(&MetadataVersion::Number(2), &metadata2.root,),
Track::store_meta(&MetadataVersion::None, &metadata3.root),
Track::store_meta(&MetadataVersion::Number(3), &metadata3.root,),
Track::store_meta(
&MetadataVersion::None,
metadata1.timestamp.as_ref().unwrap(),
),
Track::store_meta(&MetadataVersion::None, metadata1.snapshot.as_ref().unwrap(),),
Track::store_meta(&MetadataVersion::None, metadata1.targets.as_ref().unwrap(),),
],
);
}
#[test]
fn test_fetch_target_description_standard() {
block_on(test_fetch_target_description(
"standard/metadata".to_string(),
TargetDescription::from_reader(
"target with no custom metadata".as_bytes(),
&[HashAlgorithm::Sha256],
)
.unwrap(),
));
}
#[test]
fn test_fetch_target_description_custom_empty() {
block_on(test_fetch_target_description(
"custom-empty".to_string(),
TargetDescription::from_reader_with_custom(
"target with empty custom metadata".as_bytes(),
&[HashAlgorithm::Sha256],
hashmap!(),
)
.unwrap(),
));
}
#[test]
fn test_fetch_target_description_custom() {
block_on(test_fetch_target_description(
"custom/metadata".to_string(),
TargetDescription::from_reader_with_custom(
"target with lots of custom metadata".as_bytes(),
&[HashAlgorithm::Sha256],
hashmap!(
"string".to_string() => json!("string"),
"bool".to_string() => json!(true),
"int".to_string() => json!(42),
"object".to_string() => json!({
"string": json!("string"),
"bool": json!(true),
"int": json!(42),
}),
"array".to_string() => json!([1, 2, 3]),
),
)
.unwrap(),
));
}
async fn test_fetch_target_description(path: String, expected_description: TargetDescription) {
let remote = EphemeralRepository::<Json>::new();
let metadata = RepoBuilder::new(&remote)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.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())
.timestamp_key(KEYS[0].public().clone())
})
.with_targets_builder(|bld| {
bld.insert_target_description(
VirtualTargetPath::new(path.clone()).unwrap(),
expected_description.clone(),
)
})
.commit()
.await
.unwrap();
let mut client = Client::with_trusted_root(
Config::default(),
&metadata.root,
EphemeralRepository::new(),
remote,
)
.await
.unwrap();
assert_matches!(client.update().await, Ok(true));
let description = client
.fetch_target_description(&TargetPath::new(path).unwrap())
.await
.unwrap();
assert_eq!(description, expected_description);
}
#[test]
fn update_fails_if_cannot_write_to_repo() {
block_on(async {
let remote = EphemeralRepository::<Json>::new();
let _ = RepoBuilder::new(&remote)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(1)
.commit()
.await
.unwrap();
let local = ErrorRepository::new(EphemeralRepository::new());
let mut client = Client::with_trusted_root_keys(
Config::default(),
&MetadataVersion::Number(1),
1,
once(&KEYS[0].public().clone()),
&local,
&remote,
)
.await
.unwrap();
assert_matches!(client.update().await, Ok(true));
let _ = RepoBuilder::new(&remote)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(2)
.snapshot_version(2)
.timestamp_version(2)
.commit()
.await
.unwrap();
local.fail_metadata_stores(true);
assert_matches!(client.update().await, Err(Error::Encoding(_)));
assert_matches!(client.update().await, Err(Error::Encoding(_)));
assert_matches!(client.update().await, Err(Error::Encoding(_)));
assert_matches!(client.update().await, Ok(false));
});
}
#[test]
fn with_trusted_methods_return_correct_metadata() {
block_on(async {
let local = EphemeralRepository::<Json>::new();
let remote = EphemeralRepository::<Json>::new();
let metadata1 = RepoBuilder::new(&local)
.root_keys(vec![&KEYS[0]])
.targets_keys(vec![&KEYS[0]])
.snapshot_keys(vec![&KEYS[0]])
.timestamp_keys(vec![&KEYS[0]])
.with_root_builder(|bld| {
bld.version(1)
.consistent_snapshot(true)
.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())
.timestamp_key(KEYS[0].public().clone())
})
.targets_version(1)
.commit()
.await
.unwrap();
let track_local = TrackRepository::new(&local);
let track_remote = TrackRepository::new(&remote);
let client = Client::with_trusted_root(
Config::default(),
&metadata1.root,
&track_local,
&track_remote,
)
.await
.unwrap();
assert_eq!(client.trusted_targets(), client.tuf.trusted_targets());
assert_eq!(client.trusted_snapshot(), client.tuf.trusted_snapshot());
assert_eq!(client.trusted_timestamp(), client.tuf.trusted_timestamp());
assert_eq!(
client.trusted_delegations(),
client.tuf.trusted_delegations()
);
})
}
}