blob: 11393836cb569c241caff0758289889f19e2bfc7 [file] [log] [blame]
//! 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 futures_executor::block_on;
use futures_util::io::AsyncReadExt;
use matches::assert_matches;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use tuf::client::{Client, Config};
use tuf::crypto::PublicKey;
use tuf::interchange::Json;
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(
Path::new("tests")
.join("fuchsia-go-tuf-5527fe")
.join("consistent-snapshot-false"),
);
}
#[test]
fn fuchsia_go_tuf_consistent_snapshot_true() {
test_key_rotation(
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(
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(
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(
Path::new("tests")
.join("metadata")
.join("consistent-snapshot-false"),
);
}
#[test]
fn rust_tuf_identity_consistent_snapshot_true() {
test_key_rotation(
Path::new("tests")
.join("metadata")
.join("consistent-snapshot-true"),
);
}
fn test_key_rotation(dir: PathBuf) {
block_on(async {
let mut suite = TestKeyRotation::new(dir);
suite.run_tests().await;
})
}
/// TestKeyRotation is the main driver for running the key rotation tests.
struct TestKeyRotation {
/// The paths to each test step directory.
test_steps: Vec<PathBuf>,
/// The local repository used to store the local metadata.
local: EphemeralRepository<Json>,
/// 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 TestKeyRotation {
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(&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,
&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();
assert_matches!(
client.fetch_target_to_writer(&target_path, &mut buf).await,
Ok(())
);
assert_eq!(&String::from_utf8(buf).unwrap(), expected);
}
}
}
/// Extract the initial key ids from the first step.
async fn extract_keys(dir: &Path) -> Vec<PublicKey> {
let remote = init_remote(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), None, None)
.await
.unwrap();
reader.read_to_end(&mut buf).await.unwrap();
let metadata = RawSignedMetadata::<Json, RootMetadata>::new(buf)
.parse_untrusted()
.unwrap()
.assume_valid()
.unwrap();
metadata.root_keys().cloned().collect()
}
fn init_remote(dir: &Path) -> Result<FileSystemRepository<Json>> {
FileSystemRepositoryBuilder::new(dir)
.metadata_prefix(Path::new("repository"))
.targets_prefix(Path::new("repository").join("targets"))
.build()
}