blob: ccd8ca7a9c036ebda5e827ffc3056723f276c722 [file]
//! Test Interopability with other TUF Clients
//!
//! This tests how rust-tuf's compatibility with other TUF clients. Currently the only test is to
//! verify key rotation works correctly, but this will eventually be extended to cover other
//! scenarios.
//!
//! Each client should generate metadata that follows this scheme:
//!
//! tests/interop/
//! |- $client/
//! |- consistent-snapshot-false/
//! |- 0/
//! |- repository/
//! |- targets/
//! |- $hash.0
//! |- root.json
//! |- timestamp.json
//! |- ...
//! |- 1/
//! |- repository/
//! |- targets/
//! |- $hash.0
//! |- $hash.1
//! |- ...
//! |- consistent-snapshot-true
//! |- 0/
//! |- ...
//!
//! Specifically, in each client directory, it has two broad categories - one to verify rust-tuf
//! works with `consistent_snapshot` being `false`, and one with `consistent_snapshot` being
//! `true`. Inside each directory is an ordered series of repositories. The test will initialize,
//! and then use each "step" directory as the remote server, to simulate the metadata transforming
//! over time. Finally, each repository should also contain a single target that corresponds to the
//! step name, that just contains the name of the step. This is used to verify that we can still
//! download targets at each step of the test.
use assert_matches::assert_matches;
use futures_executor::block_on;
use futures_util::io::AsyncReadExt;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use tuf::client::{Client, Config};
use tuf::crypto::PublicKey;
use tuf::interchange::{DataInterchange, Json, JsonPretty};
use tuf::metadata::{
MetadataPath, MetadataVersion, RawSignedMetadata, Role, RootMetadata, TargetPath,
};
use tuf::repository::{
EphemeralRepository, FileSystemRepository, FileSystemRepositoryBuilder, RepositoryProvider,
};
use tuf::Result;
#[test]
fn fuchsia_go_tuf_consistent_snapshot_false() {
test_key_rotation::<Json>(
Path::new("tests")
.join("fuchsia-go-tuf-5527fe")
.join("consistent-snapshot-false"),
);
}
#[test]
fn fuchsia_go_tuf_consistent_snapshot_true() {
test_key_rotation::<Json>(
Path::new("tests")
.join("fuchsia-go-tuf-5527fe")
.join("consistent-snapshot-true"),
);
}
#[test]
fn fuchsia_go_tuf_transition_m4_consistent_snapshot_false() {
test_key_rotation::<Json>(
Path::new("tests")
.join("fuchsia-go-tuf-transition-M4")
.join("consistent-snapshot-false"),
);
}
#[test]
fn fuchsia_go_tuf_transition_m4_consistent_snapshot_true() {
test_key_rotation::<Json>(
Path::new("tests")
.join("fuchsia-go-tuf-transition-M4")
.join("consistent-snapshot-true"),
);
}
// Tests to catch changes to the way we generate metadata.
#[test]
fn rust_tuf_identity_consistent_snapshot_false() {
test_key_rotation::<JsonPretty>(
Path::new("tests")
.join("metadata")
.join("consistent-snapshot-false"),
);
}
#[test]
fn rust_tuf_identity_consistent_snapshot_true() {
test_key_rotation::<JsonPretty>(
Path::new("tests")
.join("metadata")
.join("consistent-snapshot-true"),
);
}
fn test_key_rotation<D>(dir: PathBuf)
where
D: DataInterchange + Sync,
{
block_on(async {
let mut suite = TestKeyRotation::<D>::new(dir);
suite.run_tests().await;
})
}
/// TestKeyRotation is the main driver for running the key rotation tests.
struct TestKeyRotation<D>
where
D: DataInterchange + Sync,
{
/// The paths to each test step directory.
test_steps: Vec<PathBuf>,
/// The local repository used to store the local metadata.
local: EphemeralRepository<D>,
/// The targets we expect each step of the repository to contain. It will contain a target for
/// each step we've processed, named for the first step it appeared in.
expected_targets: BTreeMap<TargetPath, String>,
}
impl<D> TestKeyRotation<D>
where
D: DataInterchange + Sync,
{
fn new(test_dir: PathBuf) -> Self {
let mut test_steps = Vec::new();
for entry in test_dir.read_dir().unwrap() {
let entry = entry.unwrap();
if entry.file_type().unwrap().is_dir() {
test_steps.push(entry.path());
}
}
// Make sure the steps are in order, or else the expected_targets will be incorrect.
test_steps.sort();
TestKeyRotation {
test_steps,
local: EphemeralRepository::new(),
expected_targets: BTreeMap::new(),
}
}
async fn run_tests(&mut self) {
let mut init = true;
let mut public_keys = Vec::new();
for step_dir in self.test_steps.clone() {
// Extract the keys from the first step.
if init {
init = false;
public_keys = extract_keys::<D>(&step_dir).await;
}
self.run_test_step(&public_keys, step_dir).await;
}
}
async fn run_test_step(&mut self, public_keys: &[PublicKey], dir: PathBuf) {
let remote = init_remote(&dir).unwrap();
// Connect to the client with our initial keys.
let mut client = Client::with_trusted_root_keys(
Config::default(),
MetadataVersion::Number(1),
1,
public_keys,
&mut self.local,
remote,
)
.await
.expect("client to open");
// Update our TUF metadata. The first time should report there is new metadata, the second
// time should not.
assert_matches!(client.update().await, Ok(true));
assert_matches!(client.update().await, Ok(false));
// Add the expected target to our target list.
let file_name = dir.file_name().unwrap().to_str().unwrap().to_string();
let target_path = TargetPath::new(file_name.clone()).unwrap();
self.expected_targets.insert(target_path, file_name);
// fetch all the targets and check they have the correct content
for (target_path, expected) in self.expected_targets.iter() {
let mut buf = Vec::new();
let rdr = client.fetch_target(target_path).await.unwrap();
futures_util::io::copy(rdr, &mut buf).await.unwrap();
assert_eq!(&String::from_utf8(buf).unwrap(), expected);
}
}
}
/// Extract the initial key ids from the first step.
async fn extract_keys<D>(dir: &Path) -> Vec<PublicKey>
where
D: DataInterchange + Sync,
{
let remote = init_remote::<D>(dir).unwrap();
let root_path = MetadataPath::from_role(&Role::Root);
let mut buf = Vec::new();
let mut reader = remote
.fetch_metadata(&root_path, MetadataVersion::Number(1))
.await
.unwrap();
reader.read_to_end(&mut buf).await.unwrap();
let metadata = RawSignedMetadata::<D, RootMetadata>::new(buf)
.parse_untrusted()
.unwrap()
.assume_valid()
.unwrap();
metadata.root_keys().cloned().collect()
}
fn init_remote<D>(dir: &Path) -> Result<FileSystemRepository<D>>
where
D: DataInterchange + Sync,
{
FileSystemRepositoryBuilder::new(dir)
.metadata_prefix(Path::new("repository"))
.targets_prefix(Path::new("repository").join("targets"))
.build()
}
#[test]
fn test_metadata_generation_does_not_change_consistent_snapshot_false() {
block_on(async {
let dir = tempfile::TempDir::new().unwrap();
interop_tests::generate_repos(
&Path::new("tests").join("metadata").join("keys.json"),
dir.path(),
false,
)
.await
.unwrap();
compare_dirs(
&Path::new("tests")
.join("metadata")
.join("consistent-snapshot-false"),
dir.path(),
);
});
}
#[test]
fn test_metadata_generation_does_not_change_consistent_snapshot_true() {
block_on(async {
let dir = tempfile::TempDir::new().unwrap();
interop_tests::generate_repos(
&Path::new("tests").join("metadata").join("keys.json"),
dir.path(),
true,
)
.await
.unwrap();
compare_dirs(
&Path::new("tests")
.join("metadata")
.join("consistent-snapshot-true"),
dir.path(),
);
});
}
fn compare_dirs(expected: &Path, actual: &Path) {
assert!(expected.exists());
assert!(actual.exists());
let expected_entries = interop_tests::read_dir_files(expected);
let actual_entries = interop_tests::read_dir_files(actual);
assert_eq!(
expected_entries.keys().collect::<Vec<_>>(),
actual_entries.keys().collect::<Vec<_>>()
);
for key in expected_entries.keys() {
assert_eq!(expected_entries.get(key), actual_entries.get(key));
}
}