From bc18e39be6a4dad1d90eab4229888245468c240b Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 14:28:57 +0200 Subject: [PATCH 01/10] vendor: add libdd-tuf-rust from DataDog/rust-tuf@eb129cc Vendor the tuf crate from https://github.com/DataDog/rust-tuf at commit eb129ccad320b11e8bf99d2f0ff2c415a0795ccb into vendor/tuf/, renaming the package from 'tuf' to 'libdd-tuf-rust' (the [lib] name stays 'tuf' so all import paths in the codebase remain unchanged). libdd-remote-config now depends on the local path instead of a git reference, using a package alias so no import paths need to change: tuf = { package = "libdd-tuf-rust", path = "../vendor/tuf", ... } --- Cargo.lock | 139 +- Cargo.toml | 1 + libdd-remote-config/Cargo.toml | 2 +- vendor/tuf/Cargo.toml | 55 + vendor/tuf/LICENSE-APACHE | 1 + vendor/tuf/LICENSE-MIT | 1 + vendor/tuf/README.md | 1 + vendor/tuf/src/client.rs | 2451 ++++++++++++ vendor/tuf/src/crypto.rs | 1895 +++++++++ vendor/tuf/src/database.rs | 1910 +++++++++ vendor/tuf/src/error.rs | 183 + vendor/tuf/src/format_hex.rs | 20 + vendor/tuf/src/interchange/cjson/mod.rs | 450 +++ vendor/tuf/src/interchange/cjson/pretty.rs | 199 + vendor/tuf/src/interchange/cjson/shims.rs | 649 ++++ vendor/tuf/src/interchange/mod.rs | 37 + vendor/tuf/src/lib.rs | 128 + vendor/tuf/src/metadata.rs | 3858 +++++++++++++++++++ vendor/tuf/src/repo_builder.rs | 2963 ++++++++++++++ vendor/tuf/src/repository.rs | 744 ++++ vendor/tuf/src/repository/ephemeral.rs | 424 ++ vendor/tuf/src/repository/error_repo.rs | 81 + vendor/tuf/src/repository/file_system.rs | 618 +++ vendor/tuf/src/repository/http.rs | 459 +++ vendor/tuf/src/repository/track_repo.rs | 190 + vendor/tuf/src/util.rs | 345 ++ vendor/tuf/src/verify.rs | 165 + vendor/tuf/tests/ecdsa/ecdsa_root.canonical | 9 + vendor/tuf/tests/ecdsa/ecdsa_root.json | 1 + vendor/tuf/tests/ed25519/ed25519-1 | Bin 0 -> 64 bytes vendor/tuf/tests/ed25519/ed25519-1.pk8.der | Bin 0 -> 85 bytes vendor/tuf/tests/ed25519/ed25519-1.pub | Bin 0 -> 32 bytes vendor/tuf/tests/ed25519/ed25519-1.spki.der | Bin 0 -> 46 bytes vendor/tuf/tests/ed25519/ed25519-2.pk8.der | Bin 0 -> 85 bytes vendor/tuf/tests/ed25519/ed25519-3.pk8.der | Bin 0 -> 85 bytes vendor/tuf/tests/ed25519/ed25519-4.pk8.der | Bin 0 -> 85 bytes vendor/tuf/tests/ed25519/ed25519-5.pk8.der | Bin 0 -> 85 bytes vendor/tuf/tests/ed25519/ed25519-6.pk8.der | Bin 0 -> 85 bytes vendor/tuf/tests/integration.rs | 485 +++ vendor/tuf/tests/rsa/gen.sh | 39 + vendor/tuf/tests/rsa/rsa-2048 | Bin 0 -> 1192 bytes vendor/tuf/tests/rsa/rsa-2048.der | Bin 0 -> 1194 bytes vendor/tuf/tests/rsa/rsa-2048.pk8.der | Bin 0 -> 1220 bytes vendor/tuf/tests/rsa/rsa-2048.pkcs1.der | Bin 0 -> 270 bytes vendor/tuf/tests/rsa/rsa-2048.spki.der | Bin 0 -> 294 bytes vendor/tuf/tests/rsa/rsa-4096.der | Bin 0 -> 2348 bytes vendor/tuf/tests/rsa/rsa-4096.pk8.der | Bin 0 -> 2374 bytes vendor/tuf/tests/rsa/rsa-4096.pkcs1.der | Bin 0 -> 526 bytes vendor/tuf/tests/rsa/rsa-4096.spki.der | Bin 0 -> 550 bytes vendor/tuf/tests/simple_example.rs | 95 + 50 files changed, 18550 insertions(+), 48 deletions(-) create mode 100644 vendor/tuf/Cargo.toml create mode 120000 vendor/tuf/LICENSE-APACHE create mode 120000 vendor/tuf/LICENSE-MIT create mode 120000 vendor/tuf/README.md create mode 100644 vendor/tuf/src/client.rs create mode 100644 vendor/tuf/src/crypto.rs create mode 100644 vendor/tuf/src/database.rs create mode 100644 vendor/tuf/src/error.rs create mode 100644 vendor/tuf/src/format_hex.rs create mode 100644 vendor/tuf/src/interchange/cjson/mod.rs create mode 100644 vendor/tuf/src/interchange/cjson/pretty.rs create mode 100644 vendor/tuf/src/interchange/cjson/shims.rs create mode 100644 vendor/tuf/src/interchange/mod.rs create mode 100644 vendor/tuf/src/lib.rs create mode 100644 vendor/tuf/src/metadata.rs create mode 100644 vendor/tuf/src/repo_builder.rs create mode 100644 vendor/tuf/src/repository.rs create mode 100644 vendor/tuf/src/repository/ephemeral.rs create mode 100644 vendor/tuf/src/repository/error_repo.rs create mode 100644 vendor/tuf/src/repository/file_system.rs create mode 100644 vendor/tuf/src/repository/http.rs create mode 100644 vendor/tuf/src/repository/track_repo.rs create mode 100644 vendor/tuf/src/util.rs create mode 100644 vendor/tuf/src/verify.rs create mode 100644 vendor/tuf/tests/ecdsa/ecdsa_root.canonical create mode 100644 vendor/tuf/tests/ecdsa/ecdsa_root.json create mode 100644 vendor/tuf/tests/ed25519/ed25519-1 create mode 100644 vendor/tuf/tests/ed25519/ed25519-1.pk8.der create mode 100644 vendor/tuf/tests/ed25519/ed25519-1.pub create mode 100644 vendor/tuf/tests/ed25519/ed25519-1.spki.der create mode 100644 vendor/tuf/tests/ed25519/ed25519-2.pk8.der create mode 100644 vendor/tuf/tests/ed25519/ed25519-3.pk8.der create mode 100644 vendor/tuf/tests/ed25519/ed25519-4.pk8.der create mode 100644 vendor/tuf/tests/ed25519/ed25519-5.pk8.der create mode 100644 vendor/tuf/tests/ed25519/ed25519-6.pk8.der create mode 100644 vendor/tuf/tests/integration.rs create mode 100755 vendor/tuf/tests/rsa/gen.sh create mode 100644 vendor/tuf/tests/rsa/rsa-2048 create mode 100644 vendor/tuf/tests/rsa/rsa-2048.der create mode 100644 vendor/tuf/tests/rsa/rsa-2048.pk8.der create mode 100644 vendor/tuf/tests/rsa/rsa-2048.pkcs1.der create mode 100644 vendor/tuf/tests/rsa/rsa-2048.spki.der create mode 100644 vendor/tuf/tests/rsa/rsa-4096.der create mode 100644 vendor/tuf/tests/rsa/rsa-4096.pk8.der create mode 100644 vendor/tuf/tests/rsa/rsa-4096.pkcs1.der create mode 100644 vendor/tuf/tests/rsa/rsa-4096.spki.der create mode 100644 vendor/tuf/tests/simple_example.rs diff --git a/Cargo.lock b/Cargo.lock index 462d0e6937..966eaabe17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "assert_no_alloc" version = "1.1.2" @@ -398,7 +404,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body", + "http-body 1.0.1", "http-body-util", "itoa 1.0.11", "matchit", @@ -422,7 +428,7 @@ dependencies = [ "bytes", "futures-core", "http 1.1.0", - "http-body", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -2327,6 +2333,17 @@ dependencies = [ "itoa 1.0.11", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -2346,7 +2363,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body", + "http-body 1.0.1", "pin-project-lite", ] @@ -2381,7 +2398,7 @@ dependencies = [ "headers", "http 1.1.0", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "lazy_static", "log", @@ -2404,6 +2421,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa 1.0.11", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.6.0" @@ -2415,7 +2455,7 @@ dependencies = [ "futures-util", "h2", "http 1.1.0", - "http-body", + "http-body 1.0.1", "httparse", "httpdate", "itoa 1.0.11", @@ -2432,7 +2472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.1.0", - "hyper", + "hyper 1.6.0", "hyper-util", "rustls", "rustls-pki-types", @@ -2448,7 +2488,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", @@ -2467,8 +2507,8 @@ dependencies = [ "futures-core", "futures-util", "http 1.1.0", - "http-body", - "hyper", + "http-body 1.0.1", + "hyper 1.6.0", "ipnet", "libc", "percent-encoding", @@ -2887,10 +2927,10 @@ dependencies = [ "futures-util", "hex", "http 1.1.0", - "http-body", + "http-body 1.0.1", "http-body-util", "httparse", - "hyper", + "hyper 1.6.0", "hyper-rustls", "hyper-util", "indexmap 2.12.1", @@ -2930,7 +2970,7 @@ dependencies = [ "chrono", "crossbeam-queue", "function_name", - "hyper", + "hyper 1.6.0", "libdd-common", "serde", ] @@ -3164,7 +3204,7 @@ dependencies = [ "fastrand", "http-body-util", "httpmock", - "hyper", + "hyper 1.6.0", "hyper-util", "libdd-common", "reqwest", @@ -3311,7 +3351,7 @@ dependencies = [ "function_name", "futures", "http-body-util", - "hyper", + "hyper 1.6.0", "libc", "libdd-common", "libdd-common-ffi", @@ -3394,13 +3434,14 @@ dependencies = [ "hashbrown 0.15.1", "http 1.1.0", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "libdd-capabilities", "libdd-capabilities-impl", "libdd-common", "libdd-remote-config", "libdd-trace-protobuf", + "libdd-tuf-rust", "manual_future", "prost", "rand 0.8.5", @@ -3415,7 +3456,6 @@ dependencies = [ "tokio", "tokio-util", "tracing", - "tuf", "uuid", ] @@ -3609,10 +3649,10 @@ dependencies = [ "getrandom 0.2.15", "hex", "http 1.1.0", - "http-body", + "http-body 1.0.1", "http-body-util", "httpmock", - "hyper", + "hyper 1.6.0", "indexmap 2.12.1", "itoa 1.0.11", "libdd-capabilities", @@ -3658,6 +3698,35 @@ dependencies = [ "zip", ] +[[package]] +name = "libdd-tuf-rust" +version = "0.3.0-beta10" +dependencies = [ + "assert_matches", + "chrono", + "data-encoding", + "derp", + "futures-executor", + "futures-io", + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "itoa 0.4.8", + "lazy_static", + "log", + "maplit", + "percent-encoding", + "pretty_assertions", + "ring", + "serde", + "serde_derive", + "serde_json", + "tempfile", + "thiserror 1.0.68", + "untrusted 0.7.1", + "url", +] + [[package]] name = "libloading" version = "0.8.6" @@ -4921,9 +4990,9 @@ dependencies = [ "futures-util", "hickory-resolver", "http 1.1.0", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-rustls", "hyper-util", "js-sys", @@ -6192,9 +6261,9 @@ dependencies = [ "bytes", "h2", "http 1.1.0", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -6263,7 +6332,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body", + "http-body 1.0.1", "iri-string", "pin-project-lite", "tower", @@ -6375,30 +6444,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tuf" -version = "0.3.0-beta10" -source = "git+https://github.com/DataDog/rust-tuf/?rev=eb129ccad320b11e8bf99d2f0ff2c415a0795ccb#eb129ccad320b11e8bf99d2f0ff2c415a0795ccb" -dependencies = [ - "chrono", - "data-encoding", - "derp", - "futures-io", - "futures-util", - "http 0.2.12", - "itoa 0.4.8", - "log", - "percent-encoding", - "ring", - "serde", - "serde_derive", - "serde_json", - "tempfile", - "thiserror 1.0.68", - "untrusted 0.7.1", - "url", -] - [[package]] name = "twox-hash" version = "1.6.3" diff --git a/Cargo.toml b/Cargo.toml index ecdd1f41d7..c5aef5668a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ members = [ "libdd-log", "libdd-log-ffi", "libdd-sampling", + "vendor/tuf", ] # https://doc.rust-lang.org/cargo/reference/resolver.html diff --git a/libdd-remote-config/Cargo.toml b/libdd-remote-config/Cargo.toml index f644aba0a0..0a366c08d7 100644 --- a/libdd-remote-config/Cargo.toml +++ b/libdd-remote-config/Cargo.toml @@ -76,7 +76,7 @@ rand = { version = "0.8.5", optional = true } thiserror = "2" hashbrown = "0.15" # branch = "opw-develop" -tuf = { git = "https://github.com/DataDog/rust-tuf/", rev = "eb129ccad320b11e8bf99d2f0ff2c415a0795ccb", default-features = false, optional = true } +tuf = { package = "libdd-tuf-rust", path = "../vendor/tuf", default-features = false, optional = true } chrono = { version = "0.4", default-features = false, features = [ "clock", ], optional = true } diff --git a/vendor/tuf/Cargo.toml b/vendor/tuf/Cargo.toml new file mode 100644 index 0000000000..8145b9a8b5 --- /dev/null +++ b/vendor/tuf/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "libdd-tuf-rust" +edition = "2021" +version = "0.3.0-beta10" +authors = [ "heartsucker ", "Erick Tryzelaar " ] +description = "Library for The Update Framework (TUF)" +homepage = "https://github.com/theupdateframework/rust-tuf" +repository = "https://github.com/theupdateframework/rust-tuf" +documentation = "https://docs.rs/tuf" +readme = "README.md" +license = "MIT/Apache-2.0" +keywords = [ "security", "update", "repository" ] +categories = [ "cryptography" ] + +[lib] +name = "tuf" +path = "./src/lib.rs" + +[dependencies] +chrono = { version = "0.4", features = [ "serde" ] } +data-encoding = "2.0.0-rc.2" +derp = "0.0.14" +futures-io = "0.3.1" +futures-util = { version = "0.3.1", features = [ "io" ] } +http = "0.2.0" +hyper = { version = "0.14.15", default-features = false, features = [ "stream", "client", "http1" ], optional = true } +itoa = "0.4" +log = "0.4" +percent-encoding = "2.1" +ring = { version = "0.17" } +serde = "1" +serde_derive = "1" +serde_json = "1" +tempfile = "3" +thiserror = "1.0" +untrusted = "0.7" +url = "2" + +[dev-dependencies] +assert_matches = "1.5.0" +futures-executor = "0.3.1" +lazy_static = "1" +maplit = "1" +pretty_assertions = "1" +# Flip `serde_json::Map` to `IndexMap` during `cargo test` so sort-dependent assertions are +# meaningful (matches what consumers like Vector enable in production). +serde_json = { version = "1", features = ["preserve_order"] } + +[features] +default = ["hyper", "hyper/tcp"] + +# FIXME(https://github.com/theupdateframework/rust-tuf/issues/329) - RSA key +# support does not yet conform to the TUF spec, so it is disabled by default. +# As a warning it may experience breaking changes without a major version bump. +unstable_rsa = [] diff --git a/vendor/tuf/LICENSE-APACHE b/vendor/tuf/LICENSE-APACHE new file mode 120000 index 0000000000..965b606f33 --- /dev/null +++ b/vendor/tuf/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/vendor/tuf/LICENSE-MIT b/vendor/tuf/LICENSE-MIT new file mode 120000 index 0000000000..76219eb72e --- /dev/null +++ b/vendor/tuf/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/vendor/tuf/README.md b/vendor/tuf/README.md new file mode 120000 index 0000000000..32d46ee883 --- /dev/null +++ b/vendor/tuf/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/vendor/tuf/src/client.rs b/vendor/tuf/src/client.rs new file mode 100644 index 0000000000..ae528e5d70 --- /dev/null +++ b/vendor/tuf/src/client.rs @@ -0,0 +1,2451 @@ +//! Clients for high level interactions with TUF repositories. +//! +//! # Example +//! +//! ```no_run +//! # use futures_executor::block_on; +//! # use hyper::client::Client as HttpClient; +//! # use std::path::PathBuf; +//! # use std::str::FromStr; +//! # use tuf::{Result, Database}; +//! # use tuf::crypto::PublicKey; +//! # use tuf::client::{Client, Config}; +//! # use tuf::metadata::{RootMetadata, Role, MetadataPath, MetadataVersion}; +//! # use tuf::interchange::Json; +//! # use tuf::repository::{FileSystemRepository, HttpRepositoryBuilder}; +//! # +//! # const PUBLIC_KEY: &'static [u8] = include_bytes!("../tests/ed25519/ed25519-1.pub"); +//! # +//! # fn load_root_public_keys() -> Vec { +//! # vec![PublicKey::from_ed25519(PUBLIC_KEY).unwrap()] +//! # } +//! # +//! # fn main() -> Result<()> { +//! # block_on(async { +//! let root_public_keys = load_root_public_keys(); +//! let local = FileSystemRepository::::new(PathBuf::from("~/.rustup"))?; +//! +//! let remote = HttpRepositoryBuilder::new_with_uri( +//! "https://static.rust-lang.org/".parse::().unwrap(), +//! HttpClient::new(), +//! ) +//! .user_agent("rustup/1.4.0") +//! .build(); +//! +//! let mut client = Client::with_trusted_root_keys( +//! Config::default(), +//! MetadataVersion::Number(1), +//! 1, +//! &root_public_keys, +//! local, +//! remote, +//! ).await?; +//! +//! let _ = client.update().await?; +//! # Ok(()) +//! # }) +//! # } +//! ``` + +use chrono::{offset::Utc, DateTime}; +use futures_io::AsyncRead; +use log::{error, warn}; +use std::future::Future; +use std::pin::Pin; + +use crate::crypto::{self, HashAlgorithm, HashValue, PublicKey}; +use crate::database::Database; +use crate::error::{Error, Result}; +use crate::interchange::DataInterchange; +use crate::metadata::{ + Metadata, MetadataPath, MetadataVersion, RawSignedMetadata, RootMetadata, SnapshotMetadata, + TargetDescription, TargetPath, TargetsMetadata, +}; +use crate::repository::{Repository, RepositoryProvider, RepositoryStorage}; +use crate::verify::Verified; + +/// A client that interacts with TUF repositories. +#[derive(Debug)] +pub struct Client +where + D: DataInterchange + Sync, + L: RepositoryProvider + RepositoryStorage, + R: RepositoryProvider, +{ + config: Config, + tuf: Database, + local: Repository, + remote: Repository, +} + +impl Client +where + D: DataInterchange + Sync, + L: RepositoryProvider + RepositoryStorage, + R: RepositoryProvider, +{ + /// Create a new TUF client. It will attempt to load the latest root metadata from the local + /// repo and use it as the initial trusted root metadata, or it will return an error if it + /// cannot do so. + /// + /// **WARNING**: This is trust-on-first-use (TOFU) and offers weaker security guarantees than + /// the related methods [`Client::with_trusted_root`], [`Client::with_trusted_root_keys`]. + /// + /// # Examples + /// + /// ``` + /// # use chrono::offset::{Utc, TimeZone}; + /// # use futures_executor::block_on; + /// # use tuf::{ + /// # Error, + /// # interchange::Json, + /// # client::{Client, Config}, + /// # crypto::{Ed25519PrivateKey, PrivateKey, SignatureScheme}, + /// # metadata::{MetadataPath, MetadataVersion, Role, RootMetadataBuilder}, + /// # repository::{EphemeralRepository, RepositoryStorage}, + /// # }; + /// # fn main() -> Result<(), Error> { + /// # block_on(async { + /// # let private_key = Ed25519PrivateKey::from_pkcs8( + /// # &Ed25519PrivateKey::pkcs8()?, + /// # )?; + /// # let public_key = private_key.public().clone(); + /// let mut local = EphemeralRepository::::new(); + /// let remote = EphemeralRepository::::new(); + /// + /// let root_version = 1; + /// let root = RootMetadataBuilder::new() + /// .version(root_version) + /// .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + /// .root_key(public_key.clone()) + /// .snapshot_key(public_key.clone()) + /// .targets_key(public_key.clone()) + /// .timestamp_key(public_key.clone()) + /// .signed::(&private_key)?; + /// + /// let root_path = MetadataPath::root(); + /// let root_version = MetadataVersion::Number(root_version); + /// + /// local.store_metadata( + /// &root_path, + /// root_version, + /// &mut root.to_raw().unwrap().as_bytes() + /// ).await?; + /// + /// let client = Client::with_trusted_local( + /// Config::default(), + /// local, + /// remote, + /// ).await?; + /// # Ok(()) + /// # }) + /// # } + /// ``` + pub async fn with_trusted_local(config: Config, local: L, remote: R) -> Result { + let (local, remote) = (Repository::new(local), Repository::new(remote)); + let root_path = MetadataPath::root(); + + // FIXME should this be MetadataVersion::None so we bootstrap with the latest version? + let root_version = MetadataVersion::Number(1); + + let raw_root: RawSignedMetadata<_, RootMetadata> = local + .fetch_metadata(&root_path, root_version, config.max_root_length, vec![]) + .await?; + + let tuf = Database::from_trusted_root(&raw_root)?; + + Self::new(config, tuf, local, remote).await + } + + /// Create a new TUF client. It will trust this initial root metadata. + /// + /// # Examples + /// + /// ``` + /// # use chrono::offset::{Utc, TimeZone}; + /// # use futures_executor::block_on; + /// # use tuf::{ + /// # Error, + /// # interchange::Json, + /// # client::{Client, Config}, + /// # crypto::{Ed25519PrivateKey, KeyType, PrivateKey, SignatureScheme}, + /// # metadata::{MetadataPath, MetadataVersion, Role, RootMetadataBuilder}, + /// # repository::{EphemeralRepository}, + /// # }; + /// # fn main() -> Result<(), Error> { + /// # block_on(async { + /// # let private_key = Ed25519PrivateKey::from_pkcs8( + /// # &Ed25519PrivateKey::pkcs8()?, + /// # )?; + /// # let public_key = private_key.public().clone(); + /// let local = EphemeralRepository::::new(); + /// let remote = EphemeralRepository::::new(); + /// + /// let root_version = 1; + /// let root_threshold = 1; + /// let raw_root = RootMetadataBuilder::new() + /// .version(root_version) + /// .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + /// .root_key(public_key.clone()) + /// .root_threshold(root_threshold) + /// .snapshot_key(public_key.clone()) + /// .targets_key(public_key.clone()) + /// .timestamp_key(public_key.clone()) + /// .signed::(&private_key) + /// .unwrap() + /// .to_raw() + /// .unwrap(); + /// + /// let client = Client::with_trusted_root( + /// Config::default(), + /// &raw_root, + /// local, + /// remote, + /// ).await?; + /// # Ok(()) + /// # }) + /// # } + /// ``` + pub async fn with_trusted_root( + config: Config, + trusted_root: &RawSignedMetadata, + local: L, + remote: R, + ) -> Result { + let (local, remote) = (Repository::new(local), Repository::new(remote)); + let tuf = Database::from_trusted_root(trusted_root)?; + + Self::new(config, tuf, local, remote).await + } + + /// Create a new TUF client. It will attempt to load initial root metadata from the local and remote + /// repositories using the provided keys to pin the verification. + /// + /// # Examples + /// + /// ``` + /// # use chrono::offset::{Utc, TimeZone}; + /// # use futures_executor::block_on; + /// # use std::iter::once; + /// # use tuf::{ + /// # Error, + /// # interchange::Json, + /// # client::{Client, Config}, + /// # crypto::{Ed25519PrivateKey, KeyType, PrivateKey, SignatureScheme}, + /// # metadata::{MetadataPath, MetadataVersion, Role, RootMetadataBuilder}, + /// # repository::{EphemeralRepository, RepositoryStorage}, + /// # }; + /// # fn main() -> Result<(), Error> { + /// # block_on(async { + /// # let private_key = Ed25519PrivateKey::from_pkcs8( + /// # &Ed25519PrivateKey::pkcs8()?, + /// # )?; + /// # let public_key = private_key.public().clone(); + /// let local = EphemeralRepository::::new(); + /// let mut remote = EphemeralRepository::::new(); + /// + /// let root_version = 1; + /// let root_threshold = 1; + /// let root = RootMetadataBuilder::new() + /// .version(root_version) + /// .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + /// .root_key(public_key.clone()) + /// .root_threshold(root_threshold) + /// .snapshot_key(public_key.clone()) + /// .targets_key(public_key.clone()) + /// .timestamp_key(public_key.clone()) + /// .signed::(&private_key)?; + /// + /// let root_path = MetadataPath::root(); + /// let root_version = MetadataVersion::Number(root_version); + /// + /// remote.store_metadata( + /// &root_path, + /// root_version, + /// &mut root.to_raw().unwrap().as_bytes() + /// ).await?; + /// + /// let client = Client::with_trusted_root_keys( + /// Config::default(), + /// root_version, + /// root_threshold, + /// once(&public_key), + /// local, + /// remote, + /// ).await?; + /// # Ok(()) + /// # }) + /// # } + /// ``` + pub async fn with_trusted_root_keys<'a, I>( + config: Config, + root_version: MetadataVersion, + root_threshold: u32, + trusted_root_keys: I, + local: L, + remote: R, + ) -> Result + where + I: IntoIterator, + { + let (mut local, remote) = (Repository::new(local), Repository::new(remote)); + + let root_path = MetadataPath::root(); + let (fetched, raw_root) = fetch_metadata_from_local_or_else_remote( + &root_path, + root_version, + config.max_root_length, + vec![], + &local, + &remote, + ) + .await?; + + let tuf = + Database::from_root_with_trusted_keys(&raw_root, root_threshold, trusted_root_keys)?; + + // FIXME(#253) verify the trusted root version matches the provided version. + let root_version = MetadataVersion::Number(tuf.trusted_root().version()); + + // Only store the metadata after we have validated it. + if fetched { + // NOTE(#301): The spec only states that the unversioned root metadata needs to be + // written to non-volatile storage. This enables a method like + // `Client::with_trusted_local` to initialize trust with the latest root version. + // However, this doesn't work well when trust is established with an externally + // provided root, such as with `Clietn::with_trusted_root` or + // `Client::with_trusted_root_keys`. In those cases, it's possible those initial roots + // could be multiple versions behind the latest cached root metadata. So we'd most + // likely never use the locally cached `root.json`. + // + // Instead, as an extension to the spec, we'll write the `$VERSION.root.json` metadata + // to the local store. This will eventually enable us to initialize metadata from the + // local store (see #301). + local + .store_metadata(&root_path, root_version, &raw_root) + .await?; + + // FIXME: should we also store the root as `MetadataVersion::None`? + } + + Self::new(config, tuf, local, remote).await + } + + /// Create a new TUF client. It will trust and update the TUF database. + pub async fn from_database( + config: Config, + tuf: Database, + local: L, + remote: R, + ) -> Result { + let (local, remote) = (Repository::new(local), Repository::new(remote)); + Self::new(config, tuf, local, remote).await + } + + /// Construct a client with the given parts. + /// + /// Note: Since this was created by a prior [Client], it does not try to load + /// metadata from the included local repository, since we would have done + /// that when the prior [Client] was constructed. + pub fn from_parts(parts: Parts) -> Self { + let Parts { + config, + database, + local, + remote, + } = parts; + Self { + config, + tuf: database, + local: Repository::new(local), + remote: Repository::new(remote), + } + } + + /// Create a new TUF client. It will trust this TUF database. + async fn new( + config: Config, + mut tuf: Database, + local: Repository, + remote: Repository, + ) -> Result { + let start_time = Utc::now(); + + let res = async { + let _r = + Self::update_root_with_repos(&start_time, &config, &mut tuf, None, &local).await?; + let _ts = + Self::update_timestamp_with_repos(&start_time, &config, &mut tuf, None, &local) + .await?; + let _sn = Self::update_snapshot_with_repos( + &start_time, + &config, + &mut tuf, + None, + &local, + false, + ) + .await?; + let _ta = Self::update_targets_with_repos( + &start_time, + &config, + &mut tuf, + None, + &local, + false, + ) + .await?; + + Ok(()) + } + .await; + + match res { + Ok(()) | Err(Error::MetadataNotFound { .. }) => {} + Err(err) => { + warn!("error loading local metadata: : {}", err); + } + } + + Ok(Client { + tuf, + config, + local, + remote, + }) + } + + /// Update TUF metadata from the remote repository. + /// + /// Returns `true` if an update occurred and `false` otherwise. + pub async fn update(&mut self) -> Result { + self.update_with_start_time(&Utc::now()).await + } + + /// Update TUF metadata from the remote repository, using the specified time to determine if + /// the metadata is expired. + /// + /// Returns `true` if an update occurred and `false` otherwise. + /// + /// **WARNING**: Using an older time opens up users to a freeze attack. + pub async fn update_with_start_time(&mut self, start_time: &DateTime) -> Result { + let r = self.update_root(start_time).await?; + let ts = self.update_timestamp(start_time).await?; + let sn = self.update_snapshot(start_time).await?; + let ta = self.update_targets(start_time).await?; + + Ok(r || ts || sn || ta) + } + + /// Consumes the [Client] and returns the inner [Database] and other parts. + pub fn into_parts(self) -> Parts { + let Client { + config, + tuf, + local, + remote, + } = self; + Parts { + config, + database: tuf, + local: local.into_inner(), + remote: remote.into_inner(), + } + } + + /// Returns a reference to the TUF database. + pub fn database(&self) -> &Database { + &self.tuf + } + + /// Returns a mutable reference to the TUF database. + pub fn database_mut(&mut self) -> &mut Database { + &mut self.tuf + } + + /// Clear all trusted non-root metadata (snapshot, targets, timestamp, and + /// delegated targets) on the underlying [`Database`] while preserving the + /// currently trusted root (including any version reached via root chaining). + /// + /// Callers can use this to recover from a mid-update failure without + /// discarding a trusted root that has already been walked forward past its + /// bundled/embedded starting version. See [`Database::purge_metadata`] for + /// details. Local and remote repository caches held by this [`Client`] are + /// not affected; reset them via [`Client::local_repo_mut`] / + /// [`Client::remote_repo_mut`] if desired. + pub fn purge_metadata(&mut self) { + self.tuf.purge_metadata(); + } + + /// Returns a refrerence to the local repository. + pub fn local_repo(&self) -> &L { + self.local.as_inner() + } + + /// Returns a mutable reference to the local repository. + pub fn local_repo_mut(&mut self) -> &mut L { + self.local.as_inner_mut() + } + + /// Returns a refrerence to the remote repository. + pub fn remote_repo(&self) -> &R { + self.remote.as_inner() + } + + /// Returns a mutable reference to the remote repository. + pub fn remote_repo_mut(&mut self) -> &mut R { + self.remote.as_inner_mut() + } + + /// Update TUF root metadata from the remote repository. + /// + /// Returns `true` if an update occurred and `false` otherwise. + pub async fn update_root(&mut self, start_time: &DateTime) -> Result { + Self::update_root_with_repos( + start_time, + &self.config, + &mut self.tuf, + Some(&mut self.local), + &self.remote, + ) + .await + } + + async fn update_root_with_repos( + start_time: &DateTime, + config: &Config, + tuf: &mut Database, + mut local: Option<&mut Repository>, + remote: &Repository, + ) -> Result + where + Remote: RepositoryProvider, + { + let root_path = MetadataPath::root(); + + let mut updated = false; + + loop { + ///////////////////////////////////////// + // TUF-1.0.9 §5.1.2: + // + // Try downloading version N+1 of the root metadata file, up to some W number of + // bytes (because the size is unknown). The value for W is set by the authors of + // the application using TUF. For example, W may be tens of kilobytes. The filename + // used to download the root metadata file is of the fixed form + // VERSION_NUMBER.FILENAME.EXT (e.g., 42.root.json). If this file is not available, + // or we have downloaded more than Y number of root metadata files (because the + // exact number is as yet unknown), then go to step 5.1.9. The value for Y is set + // by the authors of the application using TUF. For example, Y may be 2^10. + + // FIXME(#306) We do not have an upper bound on the number of root metadata we'll + // fetch. This means that an attacker that's stolen the root keys could cause a client + // to fall into an infinite loop (but if an attacker has stolen the root keys, the + // client probably has worse problems to worry about). + + let next_version = MetadataVersion::Number(tuf.trusted_root().version() + 1); + let res = remote + .fetch_metadata(&root_path, next_version, config.max_root_length, vec![]) + .await; + + let raw_signed_root = match res { + Ok(raw_signed_root) => raw_signed_root, + Err(Error::MetadataNotFound { .. }) => { + break; + } + Err(err) => { + return Err(err); + } + }; + + updated = true; + + tuf.update_root(&raw_signed_root)?; + + ///////////////////////////////////////// + // TUF-1.0.9 §5.1.7: + // + // Persist root metadata. The client MUST write the file to non-volatile storage as + // FILENAME.EXT (e.g. root.json). + + if let Some(ref mut local) = local { + local + .store_metadata(&root_path, MetadataVersion::None, &raw_signed_root) + .await?; + + // NOTE(#301): See the comment in `Client::with_trusted_root_keys`. + local + .store_metadata(&root_path, next_version, &raw_signed_root) + .await?; + } + + ///////////////////////////////////////// + // TUF-1.0.9 §5.1.8: + // + // Repeat steps 5.1.1 to 5.1.8. + } + + ///////////////////////////////////////// + // TUF-1.0.9 §5.1.9: + // + // Check for a freeze attack. The latest known time MUST be lower than the expiration + // timestamp in the trusted root metadata file (version N). If the trusted root + // metadata file has expired, abort the update cycle, report the potential freeze + // attack. On the next update cycle, begin at step 5.0 and version N of the root + // metadata file. + + // TODO: Consider moving the root metadata expiration check into `tuf::Database`, since that's + // where we check timestamp/snapshot/targets/delegations for expiration. + if tuf.trusted_root().expires() <= start_time { + error!("Root metadata expired, potential freeze attack"); + return Err(Error::ExpiredMetadata(MetadataPath::root())); + } + + ///////////////////////////////////////// + // TUF-1.0.5 §5.1.10: + // + // Set whether consistent snapshots are used as per the trusted root metadata file (see + // Section 4.3). + + Ok(updated) + } + + /// Returns `true` if an update occurred and `false` otherwise. + async fn update_timestamp(&mut self, start_time: &DateTime) -> Result { + Self::update_timestamp_with_repos( + start_time, + &self.config, + &mut self.tuf, + Some(&mut self.local), + &self.remote, + ) + .await + } + + async fn update_timestamp_with_repos( + start_time: &DateTime, + config: &Config, + tuf: &mut Database, + local: Option<&mut Repository>, + remote: &Repository, + ) -> Result + where + Remote: RepositoryProvider, + { + let timestamp_path = MetadataPath::timestamp(); + + ///////////////////////////////////////// + // TUF-1.0.9 §5.2: + // + // Download the timestamp metadata file, up to X number of bytes (because the size is + // unknown). The value for X is set by the authors of the application using TUF. For + // example, X may be tens of kilobytes. The filename used to download the timestamp + // metadata file is of the fixed form FILENAME.EXT (e.g., timestamp.json). + + let raw_signed_timestamp = remote + .fetch_metadata( + ×tamp_path, + MetadataVersion::None, + config.max_timestamp_length, + vec![], + ) + .await?; + + if tuf + .update_timestamp(start_time, &raw_signed_timestamp)? + .is_some() + { + ///////////////////////////////////////// + // TUF-1.0.9 §5.2.4: + // + // Persist timestamp metadata. The client MUST write the file to non-volatile + // storage as FILENAME.EXT (e.g. timestamp.json). + + if let Some(local) = local { + local + .store_metadata( + ×tamp_path, + MetadataVersion::None, + &raw_signed_timestamp, + ) + .await?; + } + + Ok(true) + } else { + Ok(false) + } + } + + /// Returns `true` if an update occurred and `false` otherwise. + async fn update_snapshot(&mut self, start_time: &DateTime) -> Result { + let consistent_snapshot = self.tuf.trusted_root().consistent_snapshot(); + Self::update_snapshot_with_repos( + start_time, + &self.config, + &mut self.tuf, + Some(&mut self.local), + &self.remote, + consistent_snapshot, + ) + .await + } + + async fn update_snapshot_with_repos( + start_time: &DateTime, + config: &Config, + tuf: &mut Database, + local: Option<&mut Repository>, + remote: &Repository, + consistent_snapshots: bool, + ) -> Result + where + Remote: RepositoryProvider, + { + let snapshot_description = match tuf.trusted_timestamp() { + Some(ts) => Ok(ts.snapshot()), + None => Err(Error::MetadataNotFound { + path: MetadataPath::timestamp(), + version: MetadataVersion::None, + }), + }? + .clone(); + + if snapshot_description.version() + <= tuf.trusted_snapshot().map(|s| s.version()).unwrap_or(0) + { + return Ok(false); + } + + let version = if consistent_snapshots { + MetadataVersion::Number(snapshot_description.version()) + } else { + MetadataVersion::None + }; + + let snapshot_path = MetadataPath::snapshot(); + + // https://theupdateframework.github.io/specification/v1.0.26/#update-snapshot 5.5.1: + + // Download snapshot metadata file, up to either the number of bytes specified in the + // timestamp metadata file, or some Y number of bytes. + let snapshot_length = snapshot_description.length().or(config.max_snapshot_length); + + // https://theupdateframework.github.io/specification/v1.0.26/#update-snapshot 5.5.2: + // + // [...] The hashes of the new snapshot metadata file MUST match the hashes, if any, listed + // in the trusted timestamp metadata. + let snapshot_hashes = crypto::retain_supported_hashes(snapshot_description.hashes()); + + let raw_signed_snapshot = remote + .fetch_metadata(&snapshot_path, version, snapshot_length, snapshot_hashes) + .await?; + + // https://theupdateframework.github.io/specification/v1.0.26/#update-snapshot 5.5.3 through + // 5.5.6 are checked in [Database]. + if tuf.update_snapshot(start_time, &raw_signed_snapshot)? { + // https://theupdateframework.github.io/specification/v1.0.26/#update-snapshot 5.5.7: + // + // Persist snapshot metadata. The client MUST write the file to non-volatile storage as + // FILENAME.EXT (e.g. snapshot.json). + if let Some(local) = local { + local + .store_metadata(&snapshot_path, MetadataVersion::None, &raw_signed_snapshot) + .await?; + } + + Ok(true) + } else { + Ok(false) + } + } + + /// Returns `true` if an update occurred and `false` otherwise. + async fn update_targets(&mut self, start_time: &DateTime) -> Result { + let consistent_snapshot = self.tuf.trusted_root().consistent_snapshot(); + Self::update_targets_with_repos( + start_time, + &self.config, + &mut self.tuf, + Some(&mut self.local), + &self.remote, + consistent_snapshot, + ) + .await + } + + async fn update_targets_with_repos( + start_time: &DateTime, + config: &Config, + tuf: &mut Database, + local: Option<&mut Repository>, + remote: &Repository, + consistent_snapshot: bool, + ) -> Result + where + Remote: RepositoryProvider, + { + let targets_description = match tuf.trusted_snapshot() { + Some(sn) => match sn.meta().get(&MetadataPath::targets()) { + Some(d) => Ok(d), + None => Err(Error::MissingMetadataDescription { + parent_role: MetadataPath::snapshot(), + child_role: MetadataPath::targets(), + }), + }, + None => Err(Error::MetadataNotFound { + path: MetadataPath::snapshot(), + version: MetadataVersion::None, + }), + }? + .clone(); + + if targets_description.version() <= tuf.trusted_targets().map(|t| t.version()).unwrap_or(0) + { + return Ok(false); + } + + let version = if consistent_snapshot { + MetadataVersion::Number(targets_description.version()) + } else { + MetadataVersion::None + }; + + let targets_path = MetadataPath::targets(); + + // https://theupdateframework.github.io/specification/v1.0.26/#update-targets 5.6.1: + // + // Download the top-level targets metadata file, up to either the number of bytes specified + // in the snapshot metadata file, or some Z number of bytes. [...] + let targets_length = targets_description.length().or(config.max_targets_length); + + // https://theupdateframework.github.io/specification/v1.0.26/#update-targets 5.6.2: + // + // Check against snapshot role’s targets hash. The hashes of the new targets metadata file + // MUST match the hashes, if any, listed in the trusted snapshot metadata. [...] + let target_hashes = crypto::retain_supported_hashes(targets_description.hashes()); + + let raw_signed_targets = remote + .fetch_metadata(&targets_path, version, targets_length, target_hashes) + .await?; + + if tuf.update_targets(start_time, &raw_signed_targets)? { + ///////////////////////////////////////// + // TUF-1.0.9 §5.4.4: + // + // Persist targets metadata. The client MUST write the file to non-volatile storage + // as FILENAME.EXT (e.g. targets.json). + + if let Some(local) = local { + local + .store_metadata(&targets_path, MetadataVersion::None, &raw_signed_targets) + .await?; + } + + Ok(true) + } else { + Ok(false) + } + } + + /// Fetch a target from the remote repo. + /// + /// It is **critical** that none of the bytes written to the `write` are used until this future + /// returns `Ok`, as the hash of the target is not verified until all bytes are read from the + /// repository. + pub async fn fetch_target( + &mut self, + target: &TargetPath, + ) -> Result { + self.fetch_target_with_start_time(target, &Utc::now()).await + } + + /// Fetch a target from the remote repo. + /// + /// It is **critical** that none of the bytes written to the `write` are used until this future + /// returns `Ok`, as the hash of the target is not verified until all bytes are read from the + /// repository. + pub async fn fetch_target_with_start_time( + &mut self, + target: &TargetPath, + start_time: &DateTime, + ) -> Result { + let target_description = self + .fetch_target_description_with_start_time(target, start_time) + .await?; + + // TODO: Check the local repository to see if it already has the target. + self.remote + .fetch_target( + self.tuf.trusted_root().consistent_snapshot(), + target, + target_description, + ) + .await + } + + /// Fetch a target from the remote repo and write it to the local repo. + /// + /// It is **critical** that none of the bytes written to the `write` are used until this future + /// returns `Ok`, as the hash of the target is not verified until all bytes are read from the + /// repository. + pub async fn fetch_target_to_local(&mut self, target: &TargetPath) -> Result<()> { + self.fetch_target_to_local_with_start_time(target, &Utc::now()) + .await + } + + /// Fetch a target from the remote repo and write it to the local repo. + /// + /// It is **critical** that none of the bytes written to the `write` are used until this future + /// returns `Ok`, as the hash of the target is not verified until all bytes are read from the + /// repository. + pub async fn fetch_target_to_local_with_start_time( + &mut self, + target: &TargetPath, + start_time: &DateTime, + ) -> Result<()> { + let target_description = self + .fetch_target_description_with_start_time(target, start_time) + .await?; + + // Since the async read we fetch from the remote repository has internal + // lifetimes, we need to break up client into sub-objects so that rust + // won't complain about trying to borrow `&self` for the fetch, and + // `&mut self` for the store. + let Client { + tuf, local, remote, .. + } = self; + + // TODO: Check the local repository to see if it already has the target. + let mut read = remote + .fetch_target( + tuf.trusted_root().consistent_snapshot(), + target, + target_description, + ) + .await?; + + local.store_target(target, &mut read).await + } + + /// Fetch a target description from the remote repo and return it. + pub async fn fetch_target_description( + &mut self, + target: &TargetPath, + ) -> Result { + self.fetch_target_description_with_start_time(target, &Utc::now()) + .await + } + + /// Fetch a target description from the remote repo and return it. + pub async fn fetch_target_description_with_start_time( + &mut self, + target: &TargetPath, + start_time: &DateTime, + ) -> Result { + let snapshot = self + .tuf + .trusted_snapshot() + .ok_or_else(|| Error::MetadataNotFound { + path: MetadataPath::snapshot(), + version: MetadataVersion::None, + })? + .clone(); + + ///////////////////////////////////////// + // https://theupdateframework.github.io/specification/v1.0.30/#update-targets: + // + // 7. **Perform a pre-order depth-first search for metadata about the + // desired target, beginning with the top-level targets role.** Note: If + // any metadata requested in steps 5.6.7.1 - 5.6.7.2 cannot be downloaded nor + // validated, end the search and report that the target cannot be found. + + let (_, target_description) = self + .lookup_target_description(start_time, false, 0, target, &snapshot, None) + .await; + + target_description + } + + async fn lookup_target_description( + &mut self, + start_time: &DateTime, + default_terminate: bool, + current_depth: u32, + target: &TargetPath, + snapshot: &SnapshotMetadata, + targets: Option<(&Verified, MetadataPath)>, + ) -> (bool, Result) { + 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::TargetNotFound(target.clone())), + ); + } + + // these clones are dumb, but we need immutable values and not references for update + // tuf in the loop below + let (targets, targets_role) = match targets { + Some((t, role)) => (t.clone(), role), + None => match self.tuf.trusted_targets() { + Some(t) => (t.clone(), MetadataPath::targets()), + None => { + return ( + default_terminate, + Err(Error::MetadataNotFound { + path: MetadataPath::targets(), + version: MetadataVersion::None, + }), + ); + } + }, + }; + + if let Some(t) = targets.targets().get(target) { + return (default_terminate, Ok(t.clone())); + } + + for delegation in targets.delegations().roles() { + if !delegation.paths().iter().any(|p| target.is_child(p)) { + if delegation.terminating() { + return (true, Err(Error::TargetNotFound(target.clone()))); + } else { + continue; + } + } + + let role_meta = match snapshot.meta().get(delegation.name()) { + Some(m) => m, + None if delegation.terminating() => { + return (true, Err(Error::TargetNotFound(target.clone()))); + } + None => { + continue; + } + }; + + ///////////////////////////////////////// + // TUF-1.0.9 §5.4: + // + // Download the top-level targets metadata file, up to either the number of bytes + // specified in the snapshot metadata file, or some Z number of bytes. The value + // for Z is set by the authors of the application using TUF. For example, Z may be + // tens of kilobytes. If consistent snapshots are not used (see Section 7), then + // the filename used to download the targets metadata file is of the fixed form + // FILENAME.EXT (e.g., targets.json). Otherwise, the filename is of the form + // VERSION_NUMBER.FILENAME.EXT (e.g., 42.targets.json), where VERSION_NUMBER is the + // version number of the targets metadata file listed in the snapshot metadata + // file. + + let version = if self.tuf.trusted_root().consistent_snapshot() { + MetadataVersion::Number(role_meta.version()) + } else { + MetadataVersion::None + }; + + let role_length = role_meta.length().or(self.config.max_targets_length); + + // https://theupdateframework.github.io/specification/v1.0.26/#update-targets + // + // [...] The hashes of the new targets metadata file MUST match the hashes, if + // any, listed in the trusted snapshot metadata. + let role_hashes = crypto::retain_supported_hashes(role_meta.hashes()); + + let raw_signed_meta = match self + .remote + .fetch_metadata(delegation.name(), version, role_length, role_hashes) + .await + { + Ok(m) => m, + Err(e) => { + warn!("Failed to fetch metadata {:?}: {:?}", delegation.name(), e); + if delegation.terminating() { + return (true, Err(e)); + } else { + continue; + } + } + }; + + match self.tuf.update_delegated_targets( + start_time, + &targets_role, + delegation.name(), + &raw_signed_meta, + ) { + Ok(_) => { + ///////////////////////////////////////// + // TUF-1.0.9 §5.4.4: + // + // Persist targets metadata. The client MUST write the file to non-volatile + // storage as FILENAME.EXT (e.g. targets.json). + + match self + .local + .store_metadata(delegation.name(), MetadataVersion::None, &raw_signed_meta) + .await + { + Ok(_) => (), + Err(e) => { + warn!( + "Error storing metadata {:?} locally: {:?}", + delegation.name(), + e + ) + } + } + + let meta = self + .tuf + .trusted_delegations() + .get(delegation.name()) + .unwrap() + .clone(); + let f: Pin>> = + Box::pin(self.lookup_target_description( + start_time, + delegation.terminating(), + current_depth + 1, + target, + snapshot, + Some((&meta, delegation.name().clone())), + )); + let (term, res) = f.await; + + if term && res.is_err() { + return (true, res); + } + + // TODO end recursion early + } + Err(_) if !delegation.terminating() => continue, + Err(e) => return (true, Err(e)), + }; + } + + ( + default_terminate, + Err(Error::TargetNotFound(target.clone())), + ) + } +} + +/// Deconstructed parts of a [Client]. +/// +/// This allows taking apart a [Client] in order to reclaim the [Database], +/// local, and remote repositories. +#[non_exhaustive] +#[derive(Debug)] +pub struct Parts +where + D: DataInterchange + Sync, + L: RepositoryProvider + RepositoryStorage, + R: RepositoryProvider, +{ + /// The client configuration. + pub config: Config, + + /// The Tuf database, which is updated by the [Client]. + pub database: Database, + + /// The local repository, which is used to initialize the database, and + /// is updated by the [Client]. + pub local: L, + + /// The remote repository, which is used by the client to update the database. + pub remote: R, +} + +/// Helper function that first tries to fetch the metadata from the local store, and if it doesn't +/// exist or does and fails to parse, try fetching it from the remote store. +async fn fetch_metadata_from_local_or_else_remote<'a, D, L, R, M>( + path: &'a MetadataPath, + version: MetadataVersion, + max_length: Option, + hashes: Vec<(&'static HashAlgorithm, HashValue)>, + local: &'a Repository, + remote: &'a Repository, +) -> Result<(bool, RawSignedMetadata)> +where + D: DataInterchange + Sync, + L: RepositoryProvider + RepositoryStorage, + R: RepositoryProvider, + M: Metadata + 'static, +{ + match local + .fetch_metadata(path, version, max_length, hashes.clone()) + .await + { + Ok(raw_meta) => Ok((false, raw_meta)), + Err(Error::MetadataNotFound { .. }) => { + let raw_meta = remote + .fetch_metadata(path, version, max_length, hashes) + .await?; + Ok((true, raw_meta)) + } + Err(err) => Err(err), + } +} + +/// Configuration for a TUF `Client`. +/// +/// # Defaults +/// +/// The following values are considered reasonably safe defaults, however these values may change +/// as this crate moves out of beta. If you are concered about them changing, you should use the +/// `ConfigBuilder` and set your own values. +/// +/// ``` +/// # use tuf::client::{Config}; +/// let config = Config::default(); +/// assert_eq!(config.max_root_length(), &Some(500 * 1024)); +/// assert_eq!(config.max_timestamp_length(), &Some(16 * 1024)); +/// assert_eq!(config.max_snapshot_length(), &Some(2000000)); +/// assert_eq!(config.max_targets_length(), &Some(5000000)); +/// assert_eq!(config.max_delegation_depth(), 8); +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Config { + max_root_length: Option, + max_timestamp_length: Option, + max_snapshot_length: Option, + max_targets_length: Option, + max_delegation_depth: u32, +} + +impl Config { + /// Initialize a `ConfigBuilder` with the default values. + pub fn build() -> ConfigBuilder { + ConfigBuilder::default() + } + + /// Return the optional maximum root metadata length. + pub fn max_root_length(&self) -> &Option { + &self.max_root_length + } + + /// Return the optional maximum timestamp metadata size. + pub fn max_timestamp_length(&self) -> &Option { + &self.max_timestamp_length + } + + /// Return the optional maximum snapshot metadata size. + pub fn max_snapshot_length(&self) -> &Option { + &self.max_snapshot_length + } + + /// Return the optional maximum targets metadata size. + pub fn max_targets_length(&self) -> &Option { + &self.max_targets_length + } + + /// The maximum number of steps used when walking the delegation graph. + pub fn max_delegation_depth(&self) -> u32 { + self.max_delegation_depth + } +} + +impl Default for Config { + fn default() -> Self { + Config { + max_root_length: Some(500 * 1024), + max_timestamp_length: Some(16 * 1024), + max_snapshot_length: Some(2000000), + max_targets_length: Some(5000000), + max_delegation_depth: 8, + } + } +} + +/// Helper for building and validating a TUF client `Config`. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct ConfigBuilder { + cfg: Config, +} + +impl ConfigBuilder { + /// Validate this builder return a `Config` if validation succeeds. + pub fn finish(self) -> Result { + Ok(self.cfg) + } + + /// Set the optional maximum download length for root metadata. + pub fn max_root_length(mut self, max: Option) -> Self { + self.cfg.max_root_length = max; + self + } + + /// Set the optional maximum download length for timestamp metadata. + pub fn max_timestamp_length(mut self, max: Option) -> Self { + self.cfg.max_timestamp_length = max; + self + } + + /// Set the optional maximum download length for snapshot metadata. + pub fn max_snapshot_length(mut self, max: Option) -> Self { + self.cfg.max_snapshot_length = max; + self + } + + /// Set the optional maximum download length for targets metadata. + pub fn max_targets_length(mut self, max: Option) -> Self { + self.cfg.max_targets_length = max; + self + } + + /// Set the maximum number of steps used when walking the delegation graph. + pub fn max_delegation_depth(mut self, max: u32) -> Self { + self.cfg.max_delegation_depth = max; + self + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::crypto::{Ed25519PrivateKey, HashAlgorithm, PrivateKey}; + use crate::interchange::Json; + use crate::metadata::{ + MetadataDescription, MetadataPath, MetadataVersion, RootMetadataBuilder, + SnapshotMetadataBuilder, TargetsMetadataBuilder, TimestampMetadataBuilder, + }; + use crate::repo_builder::RepoBuilder; + use crate::repository::{ + fetch_metadata_to_string, EphemeralRepository, ErrorRepository, Track, TrackRepository, + }; + use assert_matches::assert_matches; + use chrono::prelude::*; + use futures_executor::block_on; + use lazy_static::lazy_static; + use maplit::hashmap; + use pretty_assertions::assert_eq; + use serde_json::json; + use std::collections::HashMap; + use std::iter::once; + + lazy_static! { + static ref KEYS: Vec = { + 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() + }; + } + + #[allow(clippy::enum_variant_names)] + enum ConstructorMode { + WithTrustedLocal, + WithTrustedRoot, + WithTrustedRootKeys, + FromDatabase, + } + + #[test] + fn client_constructors_err_with_not_found() { + block_on(async { + let mut local = EphemeralRepository::::new(); + let remote = EphemeralRepository::::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(), &mut local, &remote).await, + Err(Error::MetadataNotFound { path, version }) + if path == MetadataPath::root() && version == MetadataVersion::Number(1) + ); + + assert_matches!( + Client::with_trusted_root_keys( + Config::default(), + MetadataVersion::Number(1), + 1, + once(&public_key), + local, + &remote, + ) + .await, + Err(Error::MetadataNotFound { path, version }) + if path == MetadataPath::root() && version == MetadataVersion::Number(1) + ); + }) + } + + #[test] + fn client_constructors_err_with_invalid_keys() { + block_on(async { + let mut remote = EphemeralRepository::::new(); + + let good_private_key = &KEYS[0]; + let bad_private_key = &KEYS[1]; + + let _ = RepoBuilder::create(&mut remote) + .trusted_root_keys(&[good_private_key]) + .trusted_targets_keys(&[good_private_key]) + .trusted_snapshot_keys(&[good_private_key]) + .trusted_timestamp_keys(&[good_private_key]) + .commit() + .await + .unwrap(); + + assert_matches!( + Client::with_trusted_root_keys( + Config::default(), + MetadataVersion::Number(1), + 1, + once(bad_private_key.public()), + EphemeralRepository::new(), + &remote, + ) + .await, + Err(Error::MetadataMissingSignatures { role, number_of_valid_signatures: 0, threshold: 1 }) + if role == MetadataPath::root() + ); + }) + } + + #[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, + )) + } + + #[test] + fn from_database_loads_metadata_from_local_repo() { + block_on(constructors_load_metadata_from_local_repo( + ConstructorMode::FromDatabase, + )) + } + + async fn constructors_load_metadata_from_local_repo(constructor_mode: ConstructorMode) { + // Store an expired root in the local store. + let mut local = EphemeralRepository::::new(); + let metadata1 = RepoBuilder::create(&mut local) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| { + bld.consistent_snapshot(true) + .expires(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)) + }) + .unwrap() + .commit_skip_validation() + .await + .unwrap(); + + // Remote repo has unexpired metadata. + let mut remote = EphemeralRepository::::new(); + let metadata2 = RepoBuilder::create(&mut remote) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| bld.version(2).consistent_snapshot(true)) + .unwrap() + .stage_targets_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_snapshot_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_timestamp_with_builder(|bld| bld.version(2)) + .unwrap() + .commit() + .await + .unwrap(); + + // Now, make sure that the local metadata got version 1. + let track_local = TrackRepository::new(local); + let track_remote = TrackRepository::new(remote); + + // Make sure the client initialized metadata in the right order. Each has a slightly + // different usage of the local repository. + 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().unwrap(), + 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(), + ConstructorMode::FromDatabase => Client::from_database( + Config::default(), + Database::from_trusted_root(metadata1.root().unwrap()).unwrap(), + track_local, + track_remote, + ) + .await + .unwrap(), + }; + + assert_eq!(client.tuf.trusted_root().version(), 1); + assert_eq!(client.remote_repo().take_tracks(), vec![]); + + // According to [1], "Check for freeze attack", only the root should be + // fetched since it has expired. + // + // [1]: https://theupdateframework.github.io/specification/latest/#update-root + match constructor_mode { + ConstructorMode::WithTrustedLocal => { + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::fetch_meta_found( + MetadataVersion::Number(1), + metadata1.root().unwrap() + ), + Track::FetchErr(MetadataPath::root(), MetadataVersion::Number(2)), + ], + ); + } + ConstructorMode::WithTrustedRoot => { + assert_eq!( + client.local_repo().take_tracks(), + vec![Track::FetchErr( + MetadataPath::root(), + MetadataVersion::Number(2) + )], + ); + } + ConstructorMode::WithTrustedRootKeys => { + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::fetch_meta_found( + MetadataVersion::Number(1), + metadata1.root().unwrap() + ), + Track::FetchErr(MetadataPath::root(), MetadataVersion::Number(2)), + ], + ); + } + ConstructorMode::FromDatabase => { + assert_eq!( + client.local_repo().take_tracks(), + vec![Track::FetchErr( + MetadataPath::root(), + MetadataVersion::Number(2) + )], + ); + } + }; + + assert_matches!(client.update().await, Ok(true)); + assert_eq!(client.tuf.trusted_root().version(), 2); + + // We should only fetch metadata from the remote repository and write it to the local + // repository. + assert_eq!( + client.remote_repo().take_tracks(), + vec![ + Track::fetch_meta_found(MetadataVersion::Number(2), metadata2.root().unwrap()), + Track::FetchErr(MetadataPath::root(), MetadataVersion::Number(3)), + Track::fetch_meta_found(MetadataVersion::None, metadata2.timestamp().unwrap()), + Track::fetch_meta_found(MetadataVersion::Number(2), metadata2.snapshot().unwrap()), + Track::fetch_meta_found(MetadataVersion::Number(2), metadata2.targets().unwrap()), + ], + ); + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::store_meta(MetadataVersion::None, metadata2.root().unwrap()), + Track::store_meta(MetadataVersion::Number(2), metadata2.root().unwrap()), + Track::store_meta(MetadataVersion::None, metadata2.timestamp().unwrap()), + Track::store_meta(MetadataVersion::None, metadata2.snapshot().unwrap()), + Track::store_meta(MetadataVersion::None, metadata2.targets().unwrap()), + ], + ); + + // Another update should not fetch anything. + assert_matches!(client.update().await, Ok(false)); + assert_eq!(client.tuf.trusted_root().version(), 2); + + // Make sure we only fetched the next root and timestamp, and didn't store anything. + assert_eq!( + client.remote_repo().take_tracks(), + vec![ + Track::FetchErr(MetadataPath::root(), MetadataVersion::Number(3)), + Track::fetch_meta_found(MetadataVersion::None, metadata2.timestamp().unwrap()), + ] + ); + assert_eq!(client.local_repo().take_tracks(), vec![]); + } + + #[test] + fn constructor_succeeds_with_missing_metadata() { + block_on(async { + let mut local = EphemeralRepository::::new(); + let remote = EphemeralRepository::::new(); + + // Store only a root in the local store. + let metadata1 = RepoBuilder::create(&mut local) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| bld.consistent_snapshot(true)) + .unwrap() + .skip_targets() + .skip_snapshot() + .skip_timestamp() + .commit() + .await + .unwrap(); + + let track_local = TrackRepository::new(local); + let track_remote = TrackRepository::new(remote); + + // Create a client, which should try to fetch metadata from the local store. + let client = Client::with_trusted_root( + Config::default(), + metadata1.root().unwrap(), + track_local, + track_remote, + ) + .await + .unwrap(); + + assert_eq!(client.tuf.trusted_root().version(), 1); + + // We shouldn't fetch metadata. + assert_eq!(client.remote_repo().take_tracks(), vec![]); + + // We should have tried fetching a new timestamp, but it shouldn't exist in the + // repository. + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::FetchErr(MetadataPath::root(), MetadataVersion::Number(2)), + Track::FetchErr(MetadataPath::timestamp(), MetadataVersion::None) + ], + ); + + // An update should succeed. + let mut parts = client.into_parts(); + let metadata2 = RepoBuilder::create(parts.remote.as_inner_mut()) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| bld.version(2).consistent_snapshot(true)) + .unwrap() + .stage_targets_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_snapshot_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_timestamp_with_builder(|bld| bld.version(2)) + .unwrap() + .commit() + .await + .unwrap(); + + let mut client = Client::from_parts(parts); + assert_matches!(client.update().await, Ok(true)); + assert_eq!(client.tuf.trusted_root().version(), 2); + + // We should have fetched the metadata, and written it to the local database. + assert_eq!( + client.remote_repo().take_tracks(), + vec![ + Track::fetch_meta_found(MetadataVersion::Number(2), metadata2.root().unwrap()), + Track::FetchErr(MetadataPath::root(), MetadataVersion::Number(3)), + Track::fetch_meta_found(MetadataVersion::None, metadata2.timestamp().unwrap()), + Track::fetch_meta_found( + MetadataVersion::Number(2), + metadata2.snapshot().unwrap() + ), + Track::fetch_meta_found( + MetadataVersion::Number(2), + metadata2.targets().unwrap() + ), + ], + ); + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::store_meta(MetadataVersion::None, metadata2.root().unwrap()), + Track::store_meta(MetadataVersion::Number(2), metadata2.root().unwrap()), + Track::store_meta(MetadataVersion::None, metadata2.timestamp().unwrap()), + Track::store_meta(MetadataVersion::None, metadata2.snapshot().unwrap()), + Track::store_meta(MetadataVersion::None, metadata2.targets().unwrap()), + ], + ); + }) + } + + #[test] + fn constructor_succeeds_with_expired_metadata() { + block_on(async { + let mut local = EphemeralRepository::::new(); + let remote = EphemeralRepository::::new(); + + // Store an expired root in the local store. + let metadata1 = RepoBuilder::create(&mut local) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| { + bld.version(1) + .consistent_snapshot(true) + .expires(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)) + }) + .unwrap() + .commit_skip_validation() + .await + .unwrap(); + + let metadata2 = RepoBuilder::create(&mut local) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| { + bld.version(2) + .consistent_snapshot(true) + .expires(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)) + }) + .unwrap() + .stage_targets_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_snapshot_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_timestamp_with_builder(|bld| bld.version(2)) + .unwrap() + .commit_skip_validation() + .await + .unwrap(); + + // Now, make sure that the local metadata got version 1. + let track_local = TrackRepository::new(local); + let track_remote = TrackRepository::new(remote); + + let client = Client::with_trusted_root( + Config::default(), + metadata1.root().unwrap(), + track_local, + track_remote, + ) + .await + .unwrap(); + + assert_eq!(client.tuf.trusted_root().version(), 2); + + // We shouldn't fetch metadata. + assert_eq!(client.remote_repo().take_tracks(), vec![]); + + // We should only load the root metadata, but because it's expired we don't try + // fetching the other local metadata. + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::fetch_meta_found(MetadataVersion::Number(2), metadata2.root().unwrap()), + Track::FetchErr(MetadataPath::root(), MetadataVersion::Number(3)) + ], + ); + + // An update should succeed. + let mut parts = client.into_parts(); + let _metadata3 = RepoBuilder::create(&mut parts.remote) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| { + bld.version(3) + .consistent_snapshot(true) + .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + }) + .unwrap() + .stage_targets_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_snapshot_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_timestamp_with_builder(|bld| bld.version(2)) + .unwrap() + .commit() + .await + .unwrap(); + + let mut client = Client::from_parts(parts); + 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 { + // Store a malformed timestamp in the local repository. + let mut local = EphemeralRepository::::new(); + let junk_timestamp = "junk timestamp"; + + local + .store_metadata( + &MetadataPath::timestamp(), + MetadataVersion::None, + &mut junk_timestamp.as_bytes(), + ) + .await + .unwrap(); + + // Create a normal repository on the remote server. + let mut remote = EphemeralRepository::::new(); + let metadata1 = RepoBuilder::create(&mut remote) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .commit() + .await + .unwrap(); + + // Create the client. It should ignore the malformed timestamp. + let track_local = TrackRepository::new(local); + let track_remote = TrackRepository::new(remote); + + let mut client = Client::with_trusted_root( + Config::default(), + metadata1.root().unwrap(), + track_local, + track_remote, + ) + .await + .unwrap(); + + assert_eq!(client.tuf.trusted_root().version(), 1); + + // We shouldn't fetch metadata. + assert_eq!(client.remote_repo().take_tracks(), vec![]); + + // We should only load the root metadata, but because it's expired we don't try + // fetching the other local metadata. + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::FetchErr(MetadataPath::root(), MetadataVersion::Number(2)), + Track::FetchFound { + path: MetadataPath::timestamp(), + version: MetadataVersion::None, + metadata: junk_timestamp.into(), + }, + ], + ); + + // An update should work. + 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 mut repo = EphemeralRepository::::new(); + + // First, create the initial metadata. We want to use the same non-root + // metadata, so sign it with all the keys. + let metadata1 = RepoBuilder::create(&mut repo) + .trusted_root_keys(&[&KEYS[0]]) + .signing_targets_keys(&[&KEYS[1], &KEYS[2]]) + .trusted_targets_keys(&[&KEYS[0]]) + .signing_snapshot_keys(&[&KEYS[1], &KEYS[2]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .signing_timestamp_keys(&[&KEYS[1], &KEYS[2]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| bld.consistent_snapshot(consistent_snapshot)) + .unwrap() + .commit() + .await + .unwrap(); + + let root_path = MetadataPath::root(); + let timestamp_path = MetadataPath::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; + }; + + // Now, make sure that the local metadata got version 1. + 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(); + + // Check that we tried to load metadata from the local repository. + assert_eq!( + client.remote_repo().take_tracks(), + vec![Track::fetch_found( + &root_path, + MetadataVersion::Number(1), + metadata1.root().unwrap().as_bytes() + ),] + ); + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::FetchErr(root_path.clone(), MetadataVersion::Number(1)), + Track::store_meta(MetadataVersion::Number(1), metadata1.root().unwrap()), + 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); + + // Make sure we fetched the metadata in the right order. + assert_eq!( + client.remote_repo().take_tracks(), + vec![ + Track::FetchErr(root_path.clone(), MetadataVersion::Number(2)), + Track::fetch_meta_found(MetadataVersion::None, metadata1.timestamp().unwrap()), + Track::fetch_meta_found(snapshot_version, metadata1.snapshot().unwrap()), + Track::fetch_meta_found(targets_version, metadata1.targets().unwrap()), + ] + ); + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::store_meta(MetadataVersion::None, metadata1.timestamp().unwrap()), + Track::store_meta(MetadataVersion::None, metadata1.snapshot().unwrap()), + Track::store_meta(MetadataVersion::None, metadata1.targets().unwrap()), + ], + ); + + // Another update should not fetch anything. + assert_matches!(client.update().await, Ok(false)); + assert_eq!(client.tuf.trusted_root().version(), 1); + + // Make sure we only fetched the next root and timestamp, and didn't store anything. + assert_eq!( + client.remote_repo().take_tracks(), + vec![ + Track::FetchErr(root_path.clone(), MetadataVersion::Number(2)), + Track::fetch_meta_found(MetadataVersion::None, metadata1.timestamp().unwrap()), + ] + ); + assert_eq!(client.local_repo().take_tracks(), vec![]); + + //// + // Now bump the root to version 3 + + // Make sure the version 2 is also signed by version 1's keys. + // + // Note that we write to the underlying store so TrackRepo doesn't track + // this new metadata. + let mut parts = client.into_parts(); + let metadata2 = RepoBuilder::create(parts.remote.as_inner_mut()) + .signing_root_keys(&[&KEYS[0]]) + .trusted_root_keys(&[&KEYS[1]]) + .trusted_targets_keys(&[&KEYS[1]]) + .trusted_snapshot_keys(&[&KEYS[1]]) + .trusted_timestamp_keys(&[&KEYS[1]]) + .stage_root_with_builder(|bld| bld.version(2).consistent_snapshot(consistent_snapshot)) + .unwrap() + .skip_targets() + .skip_snapshot() + .skip_timestamp() + .commit() + .await + .unwrap(); + + // Make sure the version 3 is also signed by version 2's keys. + let metadata3 = RepoBuilder::create(parts.remote.as_inner_mut()) + .signing_root_keys(&[&KEYS[1]]) + .trusted_root_keys(&[&KEYS[2]]) + .trusted_targets_keys(&[&KEYS[2]]) + .trusted_snapshot_keys(&[&KEYS[2]]) + .trusted_timestamp_keys(&[&KEYS[2]]) + .stage_root_with_builder(|bld| bld.version(3).consistent_snapshot(consistent_snapshot)) + .unwrap() + .skip_targets() + .skip_snapshot() + .skip_timestamp() + .commit() + .await + .unwrap(); + + //// + // Finally, check that the update brings us to version 3. + let mut client = Client::from_parts(parts); + assert_matches!(client.update().await, Ok(true)); + assert_eq!(client.tuf.trusted_root().version(), 3); + + // Make sure we fetched and stored the metadata in the expected order. Note that we + // re-fetch snapshot and targets because we rotated keys, which caused `tuf::Database` to delete + // the metadata. + assert_eq!( + client.remote_repo().take_tracks(), + vec![ + Track::fetch_meta_found(MetadataVersion::Number(2), metadata2.root().unwrap()), + Track::fetch_meta_found(MetadataVersion::Number(3), metadata3.root().unwrap()), + Track::FetchErr(root_path.clone(), MetadataVersion::Number(4)), + Track::fetch_meta_found(MetadataVersion::None, metadata1.timestamp().unwrap()), + Track::fetch_meta_found(snapshot_version, metadata1.snapshot().unwrap()), + Track::fetch_meta_found(targets_version, metadata1.targets().unwrap()), + ] + ); + assert_eq!( + client.local_repo().take_tracks(), + vec![ + Track::store_meta(MetadataVersion::None, metadata2.root().unwrap()), + Track::store_meta(MetadataVersion::Number(2), metadata2.root().unwrap()), + Track::store_meta(MetadataVersion::None, metadata3.root().unwrap()), + Track::store_meta(MetadataVersion::Number(3), metadata3.root().unwrap()), + Track::store_meta(MetadataVersion::None, metadata1.timestamp().unwrap()), + Track::store_meta(MetadataVersion::None, metadata1.snapshot().unwrap()), + Track::store_meta(MetadataVersion::None, metadata1.targets().unwrap()), + ], + ); + } + + #[test] + fn test_fetch_target_description_standard() { + block_on(test_fetch_target_description( + "standard/metadata".to_string(), + TargetDescription::from_slice( + "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_slice_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_slice_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) { + // Generate an ephemeral repository with a single target. + let mut remote = EphemeralRepository::::new(); + + let metadata = RepoBuilder::create(&mut remote) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root() + .unwrap() + .stage_targets_with_builder(|bld| { + bld.insert_target_description( + TargetPath::new(path.clone()).unwrap(), + expected_description.clone(), + ) + }) + .unwrap() + .commit() + .await + .unwrap(); + + // Initialize and update client. + let mut client = Client::with_trusted_root( + Config::default(), + metadata.root().unwrap(), + EphemeralRepository::new(), + remote, + ) + .await + .unwrap(); + + assert_matches!(client.update().await, Ok(true)); + + // Verify fetch_target_description returns expected target metadata + let description = client + .fetch_target_description(&TargetPath::new(path).unwrap()) + .await + .unwrap(); + + assert_eq!(description, expected_description); + } + + #[test] + fn update_eventually_succeeds_if_cannot_write_to_repo() { + block_on(async { + let mut remote = EphemeralRepository::::new(); + + // First, create the metadata. + let _ = RepoBuilder::create(&mut remote) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .commit() + .await + .unwrap(); + + // Now, make sure that the local metadata got version 1. + 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(); + + // The first update should succeed. + assert_matches!(client.update().await, Ok(true)); + + // Make sure the database is correct. + let mut parts = client.into_parts(); + assert_eq!(parts.database.trusted_root().version(), 1); + assert_eq!(parts.database.trusted_timestamp().unwrap().version(), 1); + assert_eq!(parts.database.trusted_snapshot().unwrap().version(), 1); + assert_eq!(parts.database.trusted_targets().unwrap().version(), 1); + + // Publish new metadata. + let _ = RepoBuilder::create(&mut parts.remote) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_targets_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_snapshot_with_builder(|bld| bld.version(2)) + .unwrap() + .stage_timestamp_with_builder(|bld| bld.version(2)) + .unwrap() + .commit() + .await + .unwrap(); + + // Make sure we fail to write metadata to the local store. + parts.local.fail_metadata_stores(true); + + // The second update should fail. + let mut client = Client::from_parts(parts); + assert_matches!(client.update().await, Err(Error::Encoding(_))); + + // FIXME(#297): rust-tuf diverges from the spec by throwing away the + // metadata if the root is updated. + assert_eq!(client.database().trusted_root().version(), 2); + assert_eq!(client.database().trusted_timestamp(), None); + assert_eq!(client.database().trusted_snapshot(), None); + assert_eq!(client.database().trusted_targets(), None); + + // However, due to https://github.com/theupdateframework/specification/issues/131, if + // the update is retried a few times it will still succeed. + assert_matches!(client.update().await, Err(Error::Encoding(_))); + assert_eq!(client.database().trusted_root().version(), 2); + assert_eq!(client.database().trusted_timestamp().unwrap().version(), 2); + assert_eq!(client.database().trusted_snapshot(), None); + assert_eq!(client.database().trusted_targets(), None); + + assert_matches!(client.update().await, Err(Error::Encoding(_))); + assert_eq!(client.database().trusted_root().version(), 2); + assert_eq!(client.database().trusted_timestamp().unwrap().version(), 2); + assert_eq!(client.database().trusted_snapshot().unwrap().version(), 2); + assert_eq!(client.database().trusted_targets(), None); + + assert_matches!(client.update().await, Err(Error::Encoding(_))); + assert_eq!(client.database().trusted_root().version(), 2); + assert_eq!(client.database().trusted_timestamp().unwrap().version(), 2); + assert_eq!(client.database().trusted_snapshot().unwrap().version(), 2); + assert_eq!(client.database().trusted_targets().unwrap().version(), 2); + + assert_matches!(client.update().await, Ok(false)); + assert_eq!(client.database().trusted_root().version(), 2); + assert_eq!(client.database().trusted_timestamp().unwrap().version(), 2); + assert_eq!(client.database().trusted_snapshot().unwrap().version(), 2); + assert_eq!(client.database().trusted_targets().unwrap().version(), 2); + }); + } + + #[test] + fn test_local_and_remote_repo_methods() { + block_on(async { + let local = EphemeralRepository::::new(); + let mut remote = EphemeralRepository::::new(); + + let metadata1 = RepoBuilder::create(&mut remote) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root() + .unwrap() + .stage_targets() + .unwrap() + .stage_snapshot() + .unwrap() + .stage_timestamp_with_builder(|bld| bld.version(1)) + .unwrap() + .commit() + .await + .unwrap(); + + let mut client = Client::with_trusted_root( + Config::default(), + metadata1.root().unwrap(), + local, + remote, + ) + .await + .unwrap(); + + client.update().await.unwrap(); + + // Generate some new metadata. + let metadata2 = RepoBuilder::from_database( + &mut EphemeralRepository::::new(), + client.database(), + ) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .skip_root() + .skip_targets() + .skip_snapshot() + .stage_timestamp_with_builder(|bld| bld.version(2)) + .unwrap() + .commit() + .await + .unwrap(); + + // Make sure we can update the local and remote store through the client. + client + .local_repo_mut() + .store_metadata( + &MetadataPath::timestamp(), + MetadataVersion::None, + &mut metadata2.timestamp().unwrap().as_bytes(), + ) + .await + .unwrap(); + + client + .remote_repo_mut() + .store_metadata( + &MetadataPath::timestamp(), + MetadataVersion::None, + &mut metadata2.timestamp().unwrap().as_bytes(), + ) + .await + .unwrap(); + + // Make sure we can read it back. + let timestamp2 = + String::from_utf8(metadata2.timestamp().unwrap().as_bytes().to_vec()).unwrap(); + + assert_eq!( + ×tamp2, + &fetch_metadata_to_string( + client.local_repo(), + &MetadataPath::timestamp(), + MetadataVersion::None, + ) + .await + .unwrap(), + ); + + assert_eq!( + ×tamp2, + &fetch_metadata_to_string( + client.remote_repo(), + &MetadataPath::timestamp(), + MetadataVersion::None, + ) + .await + .unwrap(), + ); + + // Finally, make sure we can update the database through the client as well. + client.database_mut().update_metadata(&metadata2).unwrap(); + assert_eq!(client.database().trusted_timestamp().unwrap().version(), 2); + }) + } + + #[test] + fn client_can_update_with_unknown_len_and_hashes() { + block_on(async { + let mut repo = EphemeralRepository::::new(); + + let root = RootMetadataBuilder::new() + .consistent_snapshot(true) + .root_key(KEYS[0].public().clone()) + .targets_key(KEYS[1].public().clone()) + .snapshot_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + repo.store_metadata( + &MetadataPath::root(), + MetadataVersion::Number(1), + &mut root.as_bytes(), + ) + .await + .unwrap(); + + let targets = TargetsMetadataBuilder::new() + .signed::(&KEYS[1]) + .unwrap() + .to_raw() + .unwrap(); + + repo.store_metadata( + &MetadataPath::targets(), + MetadataVersion::Number(1), + &mut targets.as_bytes(), + ) + .await + .unwrap(); + + // Create a targets metadata description, and deliberately don't set the metadata length + // or hashes. + let targets_description = MetadataDescription::new(1, None, HashMap::new()).unwrap(); + + let snapshot = SnapshotMetadataBuilder::new() + .insert_metadata_description(MetadataPath::targets(), targets_description) + .signed::(&KEYS[2]) + .unwrap() + .to_raw() + .unwrap(); + + repo.store_metadata( + &MetadataPath::snapshot(), + MetadataVersion::Number(1), + &mut snapshot.as_bytes(), + ) + .await + .unwrap(); + + // Create a snapshot metadata description, and deliberately don't set the metadata length + // or hashes. + let timestamp_description = MetadataDescription::new(1, None, HashMap::new()).unwrap(); + + let timestamp = + TimestampMetadataBuilder::from_metadata_description(timestamp_description) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + repo.store_metadata( + &MetadataPath::timestamp(), + MetadataVersion::None, + &mut timestamp.as_bytes(), + ) + .await + .unwrap(); + + let mut client = Client::with_trusted_root_keys( + Config::default(), + MetadataVersion::Number(1), + 1, + once(&KEYS[0].public().clone()), + EphemeralRepository::new(), + repo, + ) + .await + .unwrap(); + + assert_matches!(client.update().await, Ok(true)); + }) + } +} diff --git a/vendor/tuf/src/crypto.rs b/vendor/tuf/src/crypto.rs new file mode 100644 index 0000000000..ebf773b221 --- /dev/null +++ b/vendor/tuf/src/crypto.rs @@ -0,0 +1,1895 @@ +//! Cryptographic structures and functions. + +use { + data_encoding::{BASE64_MIME, HEXLOWER}, + derp::{self, Der, Tag}, + futures_io::AsyncRead, + futures_util::AsyncReadExt as _, + ring::{ + digest::{self, SHA256, SHA512}, + rand::SystemRandom, + signature::{Ed25519KeyPair, KeyPair, ECDSA_P256_SHA256_ASN1, ED25519}, + }, + serde::{ + de::{Deserialize, Deserializer, Error as DeserializeError}, + ser::{Error as SerializeError, Serialize, Serializer}, + }, + serde_derive::{Deserialize, Serialize}, + std::{ + cmp::Ordering, + collections::HashMap, + fmt::{self, Debug, Display}, + hash, + str::FromStr, + }, + untrusted::Input, +}; + +#[cfg(feature = "unstable_rsa")] +use { + data_encoding::BASE64URL, + ring::signature::{ + RsaKeyPair, RSA_PSS_2048_8192_SHA256, RSA_PSS_2048_8192_SHA512, RSA_PSS_SHA256, + RSA_PSS_SHA512, + }, + std::{ + io::Write, + process::{Command, Stdio}, + sync::Arc, + }, +}; + +use crate::error::{derp_error_to_error, Error, Result}; +use crate::interchange::cjson::shims; +use crate::metadata::MetadataPath; + +const HASH_ALG_PREFS: &[HashAlgorithm] = &[HashAlgorithm::Sha512, HashAlgorithm::Sha256]; + +/// 1.2.840.113549.1.1.1 rsaEncryption(PKCS #1) +#[cfg(feature = "unstable_rsa")] +const RSA_SPKI_OID: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]; + +/// 1.3.101.112 curveEd25519(EdDSA 25519 signature algorithm) +const ED25519_SPKI_OID: &[u8] = &[0x2b, 0x65, 0x70]; + +/// 1.2.840.10045.2.1 id-ecPublicKey +const EC_PUBLIC_KEY_OID: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]; + +/// 1.2.840.10045.3.1.7 secp256r1 (NIST P-256) +const P256_OID: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]; + +/// The length of an ed25519 private key in bytes +const ED25519_PRIVATE_KEY_LENGTH: usize = 32; + +/// The length of an ed25519 public key in bytes +const ED25519_PUBLIC_KEY_LENGTH: usize = 32; + +/// The length of an ed25519 keypair in bytes +const ED25519_KEYPAIR_LENGTH: usize = ED25519_PRIVATE_KEY_LENGTH + ED25519_PUBLIC_KEY_LENGTH; + +fn python_tuf_compatibility_keyid_hash_algorithms() -> Option> { + Some(vec!["sha256".to_string(), "sha512".to_string()]) +} + +/// Given a map of hash algorithms and their values and retains the supported +/// hashes. Returns an `Err` if there is no match. +/// +/// ``` +/// use std::collections::HashMap; +/// use tuf::crypto::{retain_supported_hashes, HashValue, HashAlgorithm}; +/// +/// let mut map = HashMap::new(); +/// assert!(retain_supported_hashes(&map).is_empty()); +/// +/// let sha512_value = HashValue::new(vec![0x00, 0x01]); +/// let _ = map.insert(HashAlgorithm::Sha512, sha512_value.clone()); +/// assert_eq!( +/// retain_supported_hashes(&map), +/// vec![ +/// (&HashAlgorithm::Sha512, sha512_value.clone()), +/// ], +/// ); +/// +/// let sha256_value = HashValue::new(vec![0x02, 0x03]); +/// let _ = map.insert(HashAlgorithm::Sha256, sha256_value.clone()); +/// assert_eq!( +/// retain_supported_hashes(&map), +/// vec![ +/// (&HashAlgorithm::Sha512, sha512_value.clone()), +/// (&HashAlgorithm::Sha256, sha256_value.clone()), +/// ], +/// ); +/// +/// let md5_value = HashValue::new(vec![0x04, 0x05]); +/// let _ = map.insert(HashAlgorithm::Unknown("md5".into()), md5_value); +/// assert_eq!( +/// retain_supported_hashes(&map), +/// vec![ +/// (&HashAlgorithm::Sha512, sha512_value), +/// (&HashAlgorithm::Sha256, sha256_value), +/// ], +/// ); +/// ``` +pub fn retain_supported_hashes<'a>( + hashes: &'a HashMap, +) -> Vec<(&'static HashAlgorithm, HashValue)> { + let mut data = vec![]; + for alg in HASH_ALG_PREFS { + if let Some(value) = hashes.get(alg) { + data.push((alg, value.clone())); + } + } + + data +} + +#[cfg(test)] +pub(crate) fn calculate_hash(data: &[u8], hash_alg: &HashAlgorithm) -> HashValue { + let mut context = hash_alg.digest_context().unwrap(); + context.update(data); + HashValue::new(context.finish().as_ref().to_vec()) +} + +/// Calculate the size and hash digest from a given `AsyncRead`. +pub fn calculate_hashes_from_slice( + buf: &[u8], + hash_algs: &[HashAlgorithm], +) -> Result> { + if hash_algs.is_empty() { + return Err(Error::IllegalArgument( + "Cannot provide empty set of hash algorithms".into(), + )); + } + + let mut hashes = HashMap::new(); + for alg in hash_algs { + let mut context = alg.digest_context()?; + context.update(buf); + + hashes.insert( + alg.clone(), + HashValue::new(context.finish().as_ref().to_vec()), + ); + } + + Ok(hashes) +} + +/// Calculate the size and hash digest from a given `AsyncRead`. +pub async fn calculate_hashes_from_reader( + mut read: R, + hash_algs: &[HashAlgorithm], +) -> Result<(u64, HashMap)> +where + R: AsyncRead + Unpin, +{ + if hash_algs.is_empty() { + return Err(Error::IllegalArgument( + "Cannot provide empty set of hash algorithms".into(), + )); + } + + let mut size = 0; + let mut hashes = HashMap::new(); + for alg in hash_algs { + let _ = hashes.insert(alg, alg.digest_context()?); + } + + let mut buf = vec![0; 1024]; + loop { + match read.read(&mut buf).await { + Ok(read_bytes) => { + if read_bytes == 0 { + break; + } + + size += read_bytes as u64; + + for context in hashes.values_mut() { + context.update(&buf[0..read_bytes]); + } + } + e @ Err(_) => e.map(|_| ())?, + } + } + + let hashes = hashes + .drain() + .map(|(k, v)| (k.clone(), HashValue::new(v.finish().as_ref().to_vec()))) + .collect(); + Ok((size, hashes)) +} + +fn shim_public_key( + key_type: &KeyType, + signature_scheme: &SignatureScheme, + keyid_hash_algorithms: &Option>, + public_key: &[u8], +) -> Result { + let key = match (key_type, signature_scheme) { + (KeyType::Ed25519, SignatureScheme::Ed25519) => HEXLOWER.encode(public_key), + #[cfg(feature = "unstable_rsa")] + (KeyType::Rsa, SignatureScheme::RsaSsaPssSha256) + | (KeyType::Rsa, SignatureScheme::RsaSsaPssSha512) => { + let bytes = write_spki(public_key, key_type).map_err(derp_error_to_error)?; + BASE64URL.encode(&bytes) + } + (KeyType::Ecdsa, SignatureScheme::EcdsaSha2Nistp256) => { + // PEM SPKI string emitted verbatim so the keyid matches what the signer computed. + std::str::from_utf8(public_key) + .map_err(|err| { + Error::Encoding(format!("ECDSA public key was not valid UTF-8: {err:?}")) + })? + .to_string() + } + (_, _) => { + // We don't understand this key type and/or signature scheme, so we left it as a UTF-8 string. + std::str::from_utf8(public_key) + .map_err(|err| { + Error::Encoding(format!( + "error converting public key value {:?} with key \ + type {:?} and signature scheme {:?} to a string: {:?}", + public_key, key_type, signature_scheme, err + )) + })? + .to_string() + } + }; + + Ok(shims::PublicKey::new( + key_type.clone(), + signature_scheme.clone(), + keyid_hash_algorithms.clone(), + key, + )) +} + +fn calculate_key_id( + key_type: &KeyType, + signature_scheme: &SignatureScheme, + keyid_hash_algorithms: &Option>, + public_key: &[u8], +) -> Result { + use crate::interchange::{DataInterchange, Json}; + + let public_key = shim_public_key( + key_type, + signature_scheme, + keyid_hash_algorithms, + public_key, + )?; + let public_key = Json::canonicalize(&Json::serialize(&public_key)?)?; + let mut context = digest::Context::new(&SHA256); + context.update(&public_key); + + let key_id = HEXLOWER.encode(context.finish().as_ref()); + + Ok(KeyId(key_id)) +} + +/// Wrapper type for public key's ID. +/// +/// # Calculating +/// +/// A `KeyId` is calculated as the hex digest of the SHA-256 hash of the +/// canonical form of the public key, or `hexdigest(sha256(cjson(public_key)))`. +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct KeyId(String); + +impl FromStr for KeyId { + type Err = Error; + + /// Parse a key ID from a string. + fn from_str(string: &str) -> Result { + if string.len() != 64 { + return Err(Error::IllegalArgument( + "key ID must be 64 characters long".into(), + )); + } + Ok(KeyId(string.to_owned())) + } +} + +impl Serialize for KeyId { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + self.0.serialize(ser) + } +} + +impl<'de> Deserialize<'de> for KeyId { + fn deserialize>(de: D) -> ::std::result::Result { + let string: String = Deserialize::deserialize(de)?; + KeyId::from_str(&string).map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// Cryptographic signature schemes. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SignatureScheme { + /// [Ed25519](https://ed25519.cr.yp.to/) + Ed25519, + + /// [RSASSA-PSS](https://tools.ietf.org/html/rfc5756) calculated over SHA256 + #[cfg(feature = "unstable_rsa")] + RsaSsaPssSha256, + + /// [RSASSA-PSS](https://tools.ietf.org/html/rfc5756) calculated over SHA512 + #[cfg(feature = "unstable_rsa")] + RsaSsaPssSha512, + + /// ECDSA over NIST P-256 with SHA-256 and ASN.1 DER signature encoding. + EcdsaSha2Nistp256, + + /// Placeholder for an unknown scheme. + Unknown(String), +} + +impl SignatureScheme { + /// Construct a signature scheme from a `&str`. + pub fn new(name: &str) -> Self { + match name { + "ed25519" => SignatureScheme::Ed25519, + #[cfg(feature = "unstable_rsa")] + "rsassa-pss-sha256" => SignatureScheme::RsaSsaPssSha256, + #[cfg(feature = "unstable_rsa")] + "rsassa-pss-sha512" => SignatureScheme::RsaSsaPssSha512, + // `ecdsa` is the bare alias TUF spec ≥ 1.0.32 permits. + "ecdsa-sha2-nistp256" | "ecdsa" => SignatureScheme::EcdsaSha2Nistp256, + scheme => SignatureScheme::Unknown(scheme.to_string()), + } + } + + /// Return the signature scheme as a `&str`. + pub fn as_str(&self) -> &str { + match *self { + SignatureScheme::Ed25519 => "ed25519", + #[cfg(feature = "unstable_rsa")] + SignatureScheme::RsaSsaPssSha256 => "rsassa-pss-sha256", + #[cfg(feature = "unstable_rsa")] + SignatureScheme::RsaSsaPssSha512 => "rsassa-pss-sha512", + SignatureScheme::EcdsaSha2Nistp256 => "ecdsa-sha2-nistp256", + SignatureScheme::Unknown(ref s) => s, + } + } +} + +impl Display for SignatureScheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl Serialize for SignatureScheme { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + ser.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for SignatureScheme { + fn deserialize>(de: D) -> ::std::result::Result { + let string: String = Deserialize::deserialize(de)?; + Ok(Self::new(&string)) + } +} + +/// Wrapper type for the value of a cryptographic signature. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct SignatureValue(#[serde(with = "crate::format_hex")] Vec); + +impl SignatureValue { + /// Create a new `SignatureValue` from the given bytes. + /// + /// Note: It is unlikely that you ever want to do this manually. + pub fn new(bytes: Vec) -> Self { + SignatureValue(bytes) + } + + /// Return the signature as bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl Debug for SignatureValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("SignatureValue") + .field(&HEXLOWER.encode(&self.0)) + .finish() + } +} + +/// Types of public keys. +#[non_exhaustive] +#[derive(Clone, PartialEq, Debug, Eq, Hash)] +pub enum KeyType { + /// [Ed25519](https://ed25519.cr.yp.to/) + Ed25519, + + /// [RSA](https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29) + #[cfg(feature = "unstable_rsa")] + Rsa, + + /// ECDSA. Curve is set by the paired `SignatureScheme` (only P-256 supported). + Ecdsa, + + /// Placeholder for an unknown key type. + Unknown(String), +} + +impl KeyType { + /// Construct a key type from a `&str`. + pub fn new(name: &str) -> Self { + match name { + "ed25519" => KeyType::Ed25519, + #[cfg(feature = "unstable_rsa")] + "rsa" => KeyType::Rsa, + "ecdsa" => KeyType::Ecdsa, + keytype => KeyType::Unknown(keytype.to_string()), + } + } + + /// Return the key type as a `&str`. + pub fn as_str(&self) -> &str { + match *self { + KeyType::Ed25519 => "ed25519", + #[cfg(feature = "unstable_rsa")] + KeyType::Rsa => "rsa", + KeyType::Ecdsa => "ecdsa", + KeyType::Unknown(ref s) => s, + } + } + + fn from_oid(oid: &[u8]) -> Result { + match oid { + #[cfg(feature = "unstable_rsa")] + x if x == RSA_SPKI_OID => Ok(KeyType::Rsa), + x if x == ED25519_SPKI_OID => Ok(KeyType::Ed25519), + x if x == EC_PUBLIC_KEY_OID => Ok(KeyType::Ecdsa), + x => Err(Error::Encoding(format!( + "Unknown OID: {}", + x.iter().map(|b| format!("{:x}", b)).collect::() + ))), + } + } + + fn as_oid(&self) -> Result<&'static [u8]> { + match *self { + KeyType::Ed25519 => Ok(ED25519_SPKI_OID), + #[cfg(feature = "unstable_rsa")] + KeyType::Rsa => Ok(RSA_SPKI_OID), + // ECDSA SPKI has two OIDs (id-ecPublicKey + curve); we never synthesize it. + KeyType::Ecdsa => Err(Error::UnknownKeyType("ecdsa".into())), + KeyType::Unknown(ref s) => Err(Error::UnknownKeyType(s.clone())), + } + } +} + +impl Display for KeyType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl Serialize for KeyType { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + ser.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for KeyType { + fn deserialize>(de: D) -> ::std::result::Result { + let string: String = Deserialize::deserialize(de)?; + Ok(Self::new(&string)) + } +} + +/// A structure containing information about a private key. +pub trait PrivateKey { + /// Sign a message. + fn sign(&self, msg: &[u8]) -> Result; + + /// Return the public component of the key. + fn public(&self) -> &PublicKey; +} + +/// A structure containing information about an Ed25519 private key. +pub struct Ed25519PrivateKey { + private: Ed25519KeyPair, + public: PublicKey, +} + +impl Ed25519PrivateKey { + /// Generate Ed25519 key bytes in pkcs8 format. + pub fn pkcs8() -> Result> { + Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()) + .map(|bytes| bytes.as_ref().to_vec()) + .map_err(|_| Error::Opaque("Failed to generate Ed25519 key".into())) + } + + /// Create a new `PrivateKey` from an ed25519 keypair. The keypair is a 64 byte slice, where the + /// first 32 bytes are the ed25519 seed, and the second 32 bytes are the public key. + pub fn from_ed25519(key: &[u8]) -> Result { + Self::from_ed25519_with_keyid_hash_algorithms(key, None) + } + + /// Create a new `PrivateKey` from an ed25519 keypair with a custom `keyid_hash_algorithms`. The + /// keypair is a 64 byte slice, where the first 32 bytes are the ed25519 seed, and the second 32 + /// bytes are the public key. + pub fn from_ed25519_with_keyid_hash_algorithms( + key: &[u8], + keyid_hash_algorithms: Option>, + ) -> Result { + if key.len() != ED25519_KEYPAIR_LENGTH { + return Err(Error::Encoding( + "ed25519 private keys must be 64 bytes long".into(), + )); + } + + let private_key_bytes = &key[..ED25519_PRIVATE_KEY_LENGTH]; + let public_key_bytes = &key[ED25519_PUBLIC_KEY_LENGTH..]; + + let private = Ed25519KeyPair::from_seed_and_public_key(private_key_bytes, public_key_bytes) + .map_err(|err| Error::Encoding(err.to_string()))?; + Self::from_keypair_with_keyid_hash_algorithms(private, keyid_hash_algorithms) + } + + /// Create a private key from PKCS#8v2 DER bytes. + /// + /// # Generating Keys + /// + /// ```bash + /// $ touch ed25519-private-key.pk8 + /// $ chmod 0600 ed25519-private-key.pk8 + /// ``` + /// + /// ```no_run + /// # use ring::rand::SystemRandom; + /// # use ring::signature::Ed25519KeyPair; + /// # use std::fs::File; + /// # use std::io::Write; + /// # + /// let mut file = File::open("ed25519-private-key.pk8").unwrap(); + /// let key = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap(); + /// file.write_all(key.as_ref()).unwrap() + /// ``` + pub fn from_pkcs8(der_key: &[u8]) -> Result { + Self::from_pkcs8_with_keyid_hash_algorithms( + der_key, + python_tuf_compatibility_keyid_hash_algorithms(), + ) + } + + fn from_pkcs8_with_keyid_hash_algorithms( + der_key: &[u8], + keyid_hash_algorithms: Option>, + ) -> Result { + Self::from_keypair_with_keyid_hash_algorithms( + Ed25519KeyPair::from_pkcs8(der_key) + .map_err(|_| Error::Encoding("Could not parse key as PKCS#8v2".into()))?, + keyid_hash_algorithms, + ) + } + + fn from_keypair_with_keyid_hash_algorithms( + private: Ed25519KeyPair, + keyid_hash_algorithms: Option>, + ) -> Result { + let public = PublicKey::new( + KeyType::Ed25519, + SignatureScheme::Ed25519, + keyid_hash_algorithms, + private.public_key().as_ref().to_vec(), + )?; + + Ok(Ed25519PrivateKey { private, public }) + } +} + +impl PrivateKey for Ed25519PrivateKey { + fn sign(&self, msg: &[u8]) -> Result { + debug_assert!(self.public.scheme == SignatureScheme::Ed25519); + + let value = SignatureValue(self.private.sign(msg).as_ref().into()); + Ok(Signature { + key_id: self.public.key_id().clone(), + value, + }) + } + + fn public(&self) -> &PublicKey { + &self.public + } +} + +/// A structure containing information about an Rsa private key. +#[cfg(feature = "unstable_rsa")] +pub struct RsaPrivateKey { + private: Arc, + public: PublicKey, +} + +#[cfg(feature = "unstable_rsa")] +impl RsaPrivateKey { + /// Generate RSA key bytes in pkcs8 format. + /// + /// Note: `openssl` needs to the on the `$PATH`. + pub fn pkcs8() -> Result> { + let gen = Command::new("openssl") + .args(&[ + "genpkey", + "-algorithm", + "RSA", + "-pkeyopt", + "rsa_keygen_bits:4096", + "-pkeyopt", + "rsa_keygen_pubexp:65537", + "-outform", + "der", + ]) + .output()?; + + let mut pk8 = Command::new("openssl") + .args(&[ + "pkcs8", "-inform", "der", "-topk8", "-nocrypt", "-outform", "der", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + match pk8.stdin { + Some(ref mut stdin) => stdin.write_all(&gen.stdout)?, + None => return Err(Error::Opaque("openssl has no stdin".into())), + }; + + Ok(pk8.wait_with_output()?.stdout) + } + + /// Create a private key from PKCS#8v2 DER bytes. + /// + /// # Generating Keys + /// + /// ```bash + /// $ umask 077 + /// $ openssl genpkey -algorithm RSA \ + /// -pkeyopt rsa_keygen_bits:4096 \ + /// -pkeyopt rsa_keygen_pubexp:65537 | \ + /// openssl pkcs8 -topk8 -nocrypt -outform der > rsa-4096-private-key.pk8 + /// ``` + pub fn from_pkcs8(der_key: &[u8], scheme: SignatureScheme) -> Result { + match scheme { + SignatureScheme::RsaSsaPssSha256 | SignatureScheme::RsaSsaPssSha512 => (), + _ => { + return Err(Error::IllegalArgument(format!( + "RSA keys do not support the signing scheme {:?}", + scheme + ))) + } + } + + let key = RsaKeyPair::from_pkcs8(der_key) + .map_err(|_| Error::Encoding("Could not parse key as PKCS#8v2".into()))?; + + if key.public_modulus_len() < 256 { + return Err(Error::IllegalArgument(format!( + "RSA public modulus must be 2048 or greater. Found {}", + key.public_modulus_len() * 8 + ))); + } + + let pub_key = extract_rsa_pub_from_pkcs8(der_key).map_err(derp_error_to_error)?; + + let public = PublicKey::new( + KeyType::Rsa, + scheme, + python_tuf_compatibility_keyid_hash_algorithms(), + pub_key, + )?; + let private = Arc::new(key); + + Ok(RsaPrivateKey { private, public }) + } +} + +#[cfg(feature = "unstable_rsa")] +impl PrivateKey for RsaPrivateKey { + fn sign(&self, msg: &[u8]) -> Result { + let rng = SystemRandom::new(); + let mut buf = vec![0; self.private.public_modulus_len()]; + let scheme = match &self.public.scheme { + SignatureScheme::RsaSsaPssSha256 => &RSA_PSS_SHA256, + SignatureScheme::RsaSsaPssSha512 => &RSA_PSS_SHA512, + s => unreachable!("Key {:?} can't be used with scheme {:?}", self.private, s), + }; + + self.private + .sign(scheme, &rng, msg, &mut buf) + .map_err(|_| Error::Opaque("Failed to sign message.".into()))?; + let value = SignatureValue(buf); + + Ok(Signature { + key_id: self.public.key_id().clone(), + value, + }) + } + + fn public(&self) -> &PublicKey { + &self.public + } +} + +/// A structure containing information about a public key. +#[derive(Clone, Debug)] +pub struct PublicKey { + typ: KeyType, + key_id: KeyId, + scheme: SignatureScheme, + keyid_hash_algorithms: Option>, + value: PublicKeyValue, +} + +impl PublicKey { + fn new( + typ: KeyType, + scheme: SignatureScheme, + keyid_hash_algorithms: Option>, + value: Vec, + ) -> Result { + let key_id = calculate_key_id(&typ, &scheme, &keyid_hash_algorithms, &value)?; + let value = PublicKeyValue(value); + Ok(PublicKey { + typ, + key_id, + scheme, + keyid_hash_algorithms, + value, + }) + } + + /// Parse DER bytes as an SPKI key. + /// + /// See the documentation on `KeyValue` for more information on SPKI. + pub fn from_spki(der_bytes: &[u8], scheme: SignatureScheme) -> Result { + Self::from_spki_with_keyid_hash_algorithms( + der_bytes, + scheme, + python_tuf_compatibility_keyid_hash_algorithms(), + ) + } + + /// Parse DER bytes as an SPKI key and the `keyid_hash_algorithms`. + /// + /// See the documentation on `KeyValue` for more information on SPKI. + fn from_spki_with_keyid_hash_algorithms( + der_bytes: &[u8], + scheme: SignatureScheme, + keyid_hash_algorithms: Option>, + ) -> Result { + let input = Input::from(der_bytes); + + let (typ, value) = input + .read_all(derp::Error::Read, |input| { + derp::nested(input, Tag::Sequence, |input| { + let typ = derp::nested(input, Tag::Sequence, |input| { + let typ = derp::expect_tag_and_get_value(input, Tag::Oid)?; + + let typ = KeyType::from_oid(typ.as_slice_less_safe()) + .map_err(|_| derp::Error::WrongValue)?; + + // for RSA / ed25519 this is null, so don't both parsing it + derp::read_null(input)?; + Ok(typ) + })?; + let value = derp::bit_string_with_no_unused_bits(input)?; + Ok((typ, value.as_slice_less_safe().to_vec())) + }) + }) + .map_err(derp_error_to_error)?; + + Self::new(typ, scheme, keyid_hash_algorithms, value) + } + + /// Parse ED25519 bytes as a public key. + pub fn from_ed25519>>(bytes: T) -> Result { + Self::from_ed25519_with_keyid_hash_algorithms(bytes, None) + } + + /// Parse ED25519 bytes as a public key with a custom `keyid_hash_algorithms`. + pub fn from_ed25519_with_keyid_hash_algorithms>>( + bytes: T, + keyid_hash_algorithms: Option>, + ) -> Result { + let bytes = bytes.into(); + if bytes.len() != 32 { + return Err(Error::IllegalArgument( + "ed25519 keys must be 32 bytes long".into(), + )); + } + + Self::new( + KeyType::Ed25519, + SignatureScheme::Ed25519, + keyid_hash_algorithms, + bytes, + ) + } + + /// Write the public key as SPKI DER bytes. + /// + /// See the documentation on `KeyValue` for more information on SPKI. + pub fn as_spki(&self) -> Result> { + write_spki(&self.value.0, &self.typ).map_err(derp_error_to_error) + } + + /// An immutable reference to the key's type. + pub fn typ(&self) -> &KeyType { + &self.typ + } + + /// An immutable referece to the key's authorized signing scheme. + pub fn scheme(&self) -> &SignatureScheme { + &self.scheme + } + + /// An immutable reference to the key's ID. + pub fn key_id(&self) -> &KeyId { + &self.key_id + } + + /// Return the public key as bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.value.0 + } + + /// Use this key to verify a message with a signature. + pub fn verify(&self, role: &MetadataPath, msg: &[u8], sig: &Signature) -> Result<()> { + // ECDSA: `value` holds the PEM SPKI; ring wants the raw uncompressed EC point. + let ec_point; + let alg: &dyn ring::signature::VerificationAlgorithm = match self.scheme { + SignatureScheme::Ed25519 => &ED25519, + #[cfg(feature = "unstable_rsa")] + SignatureScheme::RsaSsaPssSha256 => &RSA_PSS_2048_8192_SHA256, + #[cfg(feature = "unstable_rsa")] + SignatureScheme::RsaSsaPssSha512 => &RSA_PSS_2048_8192_SHA512, + SignatureScheme::EcdsaSha2Nistp256 => &ECDSA_P256_SHA256_ASN1, + SignatureScheme::Unknown(ref s) => { + return Err(Error::UnknownSignatureScheme(s.to_string())); + } + }; + + let key_bytes: &[u8] = match self.scheme { + SignatureScheme::EcdsaSha2Nistp256 => { + ec_point = ec_point_from_pem_spki(&self.value.0) + .map_err(|_| Error::BadSignature(role.clone()))?; + &ec_point + } + _ => &self.value.0, + }; + + let key = ring::signature::UnparsedPublicKey::new(alg, key_bytes); + key.verify(msg, &sig.value.0) + .map_err(|_| Error::BadSignature(role.clone())) + } +} + +/// PEM SPKI (NIST P-256) → raw uncompressed EC point (`0x04 || X || Y`). +fn ec_point_from_pem_spki(pem: &[u8]) -> Result> { + let text = std::str::from_utf8(pem) + .map_err(|err| Error::Encoding(format!("ECDSA PEM was not valid UTF-8: {err:?}")))?; + + let start = text + .find("-----BEGIN PUBLIC KEY-----") + .ok_or_else(|| Error::Encoding("ECDSA PEM missing BEGIN PUBLIC KEY marker".into()))?; + let after_begin = start + "-----BEGIN PUBLIC KEY-----".len(); + let end = text[after_begin..] + .find("-----END PUBLIC KEY-----") + .ok_or_else(|| Error::Encoding("ECDSA PEM missing END PUBLIC KEY marker".into()))?; + let body = &text[after_begin..after_begin + end]; + + let der = BASE64_MIME + .decode(body.as_bytes()) + .map_err(|err| Error::Encoding(format!("ECDSA PEM body was not valid base64: {err:?}")))?; + + let input = Input::from(&der); + let point = input + .read_all(derp::Error::Read, |input| { + derp::nested(input, Tag::Sequence, |input| { + derp::nested(input, Tag::Sequence, |input| { + let alg = derp::expect_tag_and_get_value(input, Tag::Oid)?; + if alg.as_slice_less_safe() != EC_PUBLIC_KEY_OID { + return Err(derp::Error::WrongValue); + } + let curve = derp::expect_tag_and_get_value(input, Tag::Oid)?; + if curve.as_slice_less_safe() != P256_OID { + return Err(derp::Error::WrongValue); + } + Ok(()) + })?; + let bits = derp::bit_string_with_no_unused_bits(input)?; + Ok(bits.as_slice_less_safe().to_vec()) + }) + }) + .map_err(derp_error_to_error)?; + + Ok(point) +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + // key_id is derived from these fields, so we ignore it. + self.typ == other.typ + && self.scheme == other.scheme + && self.keyid_hash_algorithms == other.keyid_hash_algorithms + && self.value == other.value + } +} + +impl Eq for PublicKey {} + +impl Ord for PublicKey { + fn cmp(&self, other: &Self) -> Ordering { + self.key_id.cmp(&other.key_id) + } +} + +impl PartialOrd for PublicKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.key_id.cmp(&other.key_id)) + } +} + +impl hash::Hash for PublicKey { + fn hash(&self, state: &mut H) { + // key_id is derived from these fields, so we ignore it. + self.typ.hash(state); + self.scheme.hash(state); + self.keyid_hash_algorithms.hash(state); + self.value.hash(state); + } +} + +impl Serialize for PublicKey { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + let key = shim_public_key( + &self.typ, + &self.scheme, + &self.keyid_hash_algorithms, + &self.value.0, + ) + .map_err(|e| SerializeError::custom(format!("Couldn't write key as SPKI: {:?}", e)))?; + key.serialize(ser) + } +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::PublicKey = Deserialize::deserialize(de)?; + + let key = match intermediate.keytype() { + KeyType::Ed25519 => { + if intermediate.scheme() != &SignatureScheme::Ed25519 { + return Err(DeserializeError::custom(format!( + "ed25519 key type must be used with the ed25519 signature scheme, not {:?}", + intermediate.scheme() + ))); + } + + let bytes = HEXLOWER + .decode(intermediate.public_key().as_bytes()) + .map_err(|e| { + DeserializeError::custom(format!("Couldn't parse key as HEX: {:?}", e)) + })?; + + PublicKey::from_ed25519_with_keyid_hash_algorithms( + bytes, + intermediate.keyid_hash_algorithms().clone(), + ) + .map_err(|e| { + DeserializeError::custom(format!("Couldn't parse key as ed25519: {:?}", e)) + })? + } + #[cfg(feature = "unstable_rsa")] + KeyType::Rsa => { + let bytes = BASE64URL + .decode(intermediate.public_key().as_bytes()) + .map_err(|e| DeserializeError::custom(format!("{:?}", e)))?; + + PublicKey::from_spki_with_keyid_hash_algorithms( + &bytes, + intermediate.scheme().clone(), + intermediate.keyid_hash_algorithms().clone(), + ) + .map_err(|e| { + DeserializeError::custom(format!("Couldn't parse key as SPKI: {:?}", e)) + })? + } + KeyType::Ecdsa => { + if intermediate.scheme() != &SignatureScheme::EcdsaSha2Nistp256 { + return Err(DeserializeError::custom(format!( + "ecdsa key type must be paired with ecdsa-sha2-nistp256, not {:?}", + intermediate.scheme() + ))); + } + // Validate the PEM parses as P-256 SPKI; store the bytes verbatim for keyid. + let pem_bytes = intermediate.public_key().as_bytes().to_vec(); + ec_point_from_pem_spki(&pem_bytes).map_err(|e| { + DeserializeError::custom(format!("Couldn't parse ECDSA P-256 key: {e:?}")) + })?; + PublicKey::new( + KeyType::Ecdsa, + SignatureScheme::EcdsaSha2Nistp256, + intermediate.keyid_hash_algorithms().clone(), + pem_bytes, + ) + .map_err(|e| { + DeserializeError::custom(format!("Couldn't construct ECDSA PublicKey: {e:?}")) + })? + } + KeyType::Unknown(_) => { + // We don't know this key type, so just leave it as a UTF-8 string. + PublicKey::new( + intermediate.keytype().clone(), + intermediate.scheme().clone(), + intermediate.keyid_hash_algorithms().clone(), + intermediate.public_key().as_bytes().to_vec(), + ) + .map_err(|e| DeserializeError::custom(format!("Couldn't parse key: {:?}", e)))? + } + }; + + if intermediate.keytype() != &key.typ { + return Err(DeserializeError::custom(format!( + "Key type listed in the metadata did not match the type extrated \ + from the key. {:?} vs. {:?}", + intermediate.keytype(), + key.typ, + ))); + } + + Ok(key) + } +} + +#[derive(Clone, PartialEq, Hash, Eq)] +struct PublicKeyValue(Vec); + +impl Debug for PublicKeyValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("PublicKeyValue") + .field(&HEXLOWER.encode(&self.0)) + .finish() + } +} + +/// A structure that contains a `Signature` and associated data for verifying it. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Signature { + #[serde(rename = "keyid")] + key_id: KeyId, + #[serde(rename = "sig")] + value: SignatureValue, +} + +impl Signature { + /// An immutable reference to the `KeyId` of the key that produced the signature. + pub fn key_id(&self) -> &KeyId { + &self.key_id + } + + /// An immutable reference to the `SignatureValue`. + pub fn value(&self) -> &SignatureValue { + &self.value + } +} + +impl PartialOrd for Signature { + fn partial_cmp(&self, other: &Self) -> Option { + (&self.key_id, &self.value).partial_cmp(&(&other.key_id, &other.value)) + } +} + +impl Ord for Signature { + fn cmp(&self, other: &Self) -> Ordering { + (&self.key_id, &self.value).cmp(&(&other.key_id, &other.value)) + } +} + +/// The available hash algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum HashAlgorithm { + /// SHA256 as describe in [RFC-6234](https://tools.ietf.org/html/rfc6234) + #[serde(rename = "sha256")] + Sha256, + /// SHA512 as describe in [RFC-6234](https://tools.ietf.org/html/rfc6234) + #[serde(rename = "sha512")] + Sha512, + /// Placeholder for an unknown hash algorithm. + Unknown(String), +} + +impl HashAlgorithm { + /// Create a new `digest::Context` suitable for computing the hash of some data using this hash + /// algorithm. + pub(crate) fn digest_context(&self) -> Result { + match self { + HashAlgorithm::Sha256 => Ok(digest::Context::new(&SHA256)), + HashAlgorithm::Sha512 => Ok(digest::Context::new(&SHA512)), + HashAlgorithm::Unknown(ref s) => Err(Error::IllegalArgument(format!( + "Unknown hash algorithm: {}", + s + ))), + } + } +} + +/// Wrapper for the value of a hash digest. +#[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct HashValue(#[serde(with = "crate::format_hex")] Vec); + +impl HashValue { + /// Create a new `HashValue` from the given digest bytes. + pub fn new(bytes: Vec) -> Self { + HashValue(bytes) + } + + /// An immutable reference to the bytes of the hash value. + pub fn value(&self) -> &[u8] { + &self.0 + } +} + +impl Debug for HashValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("HashValue") + .field(&HEXLOWER.encode(&self.0)) + .finish() + } +} + +impl Display for HashValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", HEXLOWER.encode(&self.0)) + } +} + +fn write_spki(public: &[u8], key_type: &KeyType) -> ::std::result::Result, derp::Error> { + let mut output = Vec::new(); + { + let mut der = Der::new(&mut output); + der.sequence(|der| { + der.sequence(|der| match key_type.as_oid().ok() { + Some(tag) => { + der.element(Tag::Oid, tag)?; + der.null() + } + None => Err(derp::Error::WrongValue), + })?; + der.bit_string(0, public) + })?; + } + + Ok(output) +} + +#[cfg(feature = "unstable_rsa")] +fn extract_rsa_pub_from_pkcs8(der_key: &[u8]) -> ::std::result::Result, derp::Error> { + let input = Input::from(der_key); + input.read_all(derp::Error::Read, |input| { + derp::nested(input, Tag::Sequence, |input| { + if derp::small_nonnegative_integer(input)? != 0 { + return Err(derp::Error::WrongValue); + } + + derp::nested(input, Tag::Sequence, |input| { + let actual_alg_id = derp::expect_tag_and_get_value(input, Tag::Oid)?; + if actual_alg_id.as_slice_less_safe() != RSA_SPKI_OID { + return Err(derp::Error::WrongValue); + } + let _ = derp::expect_tag_and_get_value(input, Tag::Null)?; + Ok(()) + })?; + + derp::nested(input, Tag::OctetString, |input| { + derp::nested(input, Tag::Sequence, |input| { + if derp::small_nonnegative_integer(input)? != 0 { + return Err(derp::Error::WrongValue); + } + + let n = derp::positive_integer(input)?; + let e = derp::positive_integer(input)?; + let _ = input.skip_to_end(); + write_pkcs1(n.as_slice_less_safe(), e.as_slice_less_safe()) + }) + }) + }) + }) +} + +#[cfg(feature = "unstable_rsa")] +fn write_pkcs1(n: &[u8], e: &[u8]) -> ::std::result::Result, derp::Error> { + let mut output = Vec::new(); + { + let mut der = Der::new(&mut output); + der.sequence(|der| { + der.positive_integer(n)?; + der.positive_integer(e) + })?; + } + + Ok(output) +} + +#[cfg(test)] +mod test { + use super::*; + use assert_matches::assert_matches; + use pretty_assertions::assert_eq; + use serde_json::{self, json}; + + #[cfg(feature = "unstable_rsa")] + mod rsa { + pub(super) const PK8_2048: &[u8] = include_bytes!("../tests/rsa/rsa-2048.pk8.der"); + pub(super) const SPKI_2048: &[u8] = include_bytes!("../tests/rsa/rsa-2048.spki.der"); + pub(super) const PCKS1_2048: &[u8] = include_bytes!("../tests/rsa/rsa-2048.pkcs1.der"); + + pub(super) const PK8_4096: &[u8] = include_bytes!("../tests/rsa/rsa-4096.pk8.der"); + pub(super) const SPKI_4096: &[u8] = include_bytes!("../tests/rsa/rsa-4096.spki.der"); + pub(super) const PCKS1_4096: &[u8] = include_bytes!("../tests/rsa/rsa-4096.pkcs1.der"); + } + + mod ed25519 { + pub(super) const PRIVATE_KEY: &[u8] = include_bytes!("../tests/ed25519/ed25519-1"); + pub(super) const PUBLIC_KEY: &[u8] = include_bytes!("../tests/ed25519/ed25519-1.pub"); + pub(super) const PK8_1: &[u8] = include_bytes!("../tests/ed25519/ed25519-1.pk8.der"); + pub(super) const SPKI_1: &[u8] = include_bytes!("../tests/ed25519/ed25519-1.spki.der"); + pub(super) const PK8_2: &[u8] = include_bytes!("../tests/ed25519/ed25519-2.pk8.der"); + } + + mod ecdsa { + // Synthetic 2-of-2 ECDSA P-256-signed root. `.canonical` is the OLPC-canonical signed + // body the signatures cover. + pub(super) const ECDSA_ROOT: &[u8] = include_bytes!("../tests/ecdsa/ecdsa_root.json"); + pub(super) const ECDSA_ROOT_CANONICAL: &[u8] = + include_bytes!("../tests/ecdsa/ecdsa_root.canonical"); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn parse_public_rsa_2048_spki() { + let key = PublicKey::from_spki(rsa::SPKI_2048, SignatureScheme::RsaSsaPssSha256).unwrap(); + assert_eq!(key.typ, KeyType::Rsa); + assert_eq!(key.scheme, SignatureScheme::RsaSsaPssSha256); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn parse_public_rsa_4096_spki() { + let key = PublicKey::from_spki(rsa::SPKI_4096, SignatureScheme::RsaSsaPssSha256).unwrap(); + assert_eq!(key.typ, KeyType::Rsa); + assert_eq!(key.scheme, SignatureScheme::RsaSsaPssSha256); + } + + #[test] + fn parse_public_ed25519_spki() { + let key = PublicKey::from_spki(ed25519::SPKI_1, SignatureScheme::Ed25519).unwrap(); + assert_eq!(key.typ, KeyType::Ed25519); + assert_eq!(key.scheme, SignatureScheme::Ed25519); + } + + #[test] + fn parse_public_ed25519() { + let key = PublicKey::from_ed25519(ed25519::PUBLIC_KEY).unwrap(); + assert_eq!( + key.key_id(), + &KeyId::from_str("e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554") + .unwrap() + ); + assert_eq!(key.typ, KeyType::Ed25519); + assert_eq!(key.scheme, SignatureScheme::Ed25519); + } + + #[test] + fn parse_public_ed25519_without_keyid_hash_algo() { + let key = + PublicKey::from_ed25519_with_keyid_hash_algorithms(ed25519::PUBLIC_KEY, None).unwrap(); + assert_eq!( + key.key_id(), + &KeyId::from_str("e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554") + .unwrap() + ); + assert_eq!(key.typ, KeyType::Ed25519); + assert_eq!(key.scheme, SignatureScheme::Ed25519); + } + + #[test] + fn parse_public_ed25519_with_keyid_hash_algo() { + let key = PublicKey::from_ed25519_with_keyid_hash_algorithms( + ed25519::PUBLIC_KEY, + python_tuf_compatibility_keyid_hash_algorithms(), + ) + .unwrap(); + assert_eq!( + key.key_id(), + &KeyId::from_str("a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a") + .unwrap(), + ); + assert_eq!(key.typ, KeyType::Ed25519); + assert_eq!(key.scheme, SignatureScheme::Ed25519); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn rsa_2048_read_pkcs8_and_sign() { + let msg = b"test"; + + let key = + RsaPrivateKey::from_pkcs8(rsa::PK8_2048, SignatureScheme::RsaSsaPssSha256).unwrap(); + let sig = key.sign(msg).unwrap(); + key.public.verify(&MetadataPath::root(), msg, &sig).unwrap(); + + let key = + RsaPrivateKey::from_pkcs8(rsa::PK8_2048, SignatureScheme::RsaSsaPssSha512).unwrap(); + let sig = key.sign(msg).unwrap(); + key.public.verify(&MetadataPath::root(), msg, &sig).unwrap(); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn rsa_4096_read_pkcs8_and_sign() { + let msg = b"test"; + + let key = + RsaPrivateKey::from_pkcs8(rsa::PK8_4096, SignatureScheme::RsaSsaPssSha256).unwrap(); + let sig = key.sign(msg).unwrap(); + key.public.verify(&MetadataPath::root(), msg, &sig).unwrap(); + + let key = + RsaPrivateKey::from_pkcs8(rsa::PK8_4096, SignatureScheme::RsaSsaPssSha512).unwrap(); + let sig = key.sign(msg).unwrap(); + key.public.verify(&MetadataPath::root(), msg, &sig).unwrap(); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn extract_pkcs1_from_rsa_2048_pkcs8() { + let res = extract_rsa_pub_from_pkcs8(rsa::PK8_2048).unwrap(); + assert_eq!(res.as_slice(), rsa::PCKS1_2048); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn extract_pkcs1_from_rsa_4096_pkcs8() { + let res = extract_rsa_pub_from_pkcs8(rsa::PK8_4096).unwrap(); + assert_eq!(res.as_slice(), rsa::PCKS1_4096); + } + + #[test] + fn ed25519_read_pkcs8_and_sign() { + let key = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_1).unwrap(); + let msg = b"test"; + + let sig = key.sign(msg).unwrap(); + + let pub_key = + PublicKey::from_spki(&key.public.as_spki().unwrap(), SignatureScheme::Ed25519).unwrap(); + + let role = MetadataPath::root(); + assert_matches!(pub_key.verify(&role, msg, &sig), Ok(())); + + // Make sure we match what ring expects. + let ring_key = ring::signature::Ed25519KeyPair::from_pkcs8(ed25519::PK8_1).unwrap(); + assert_eq!(key.public().as_bytes(), ring_key.public_key().as_ref()); + assert_eq!(sig.value().as_bytes(), ring_key.sign(msg).as_ref()); + + // Make sure verification fails with the wrong key. + let bad_pub_key = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_2) + .unwrap() + .public() + .clone(); + + assert_matches!( + bad_pub_key.verify(&role, msg, &sig), + Err(Error::BadSignature(r)) + if r == role + ); + } + + #[test] + fn ed25519_read_keypair_and_sign() { + let key = Ed25519PrivateKey::from_ed25519(ed25519::PRIVATE_KEY).unwrap(); + let pub_key = PublicKey::from_ed25519(ed25519::PUBLIC_KEY).unwrap(); + assert_eq!(key.public(), &pub_key); + + let role = MetadataPath::root(); + let msg = b"test"; + let sig = key.sign(msg).unwrap(); + assert_matches!(pub_key.verify(&role, msg, &sig), Ok(())); + + // Make sure we match what ring expects. + let ring_key = ring::signature::Ed25519KeyPair::from_pkcs8(ed25519::PK8_1).unwrap(); + assert_eq!(key.public().as_bytes(), ring_key.public_key().as_ref()); + assert_eq!(sig.value().as_bytes(), ring_key.sign(msg).as_ref()); + + // Make sure verification fails with the wrong key. + let bad_pub_key = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_2) + .unwrap() + .public() + .clone(); + + assert_matches!( + bad_pub_key.verify(&role, msg, &sig), + Err(Error::BadSignature(r)) + if r == role + ); + } + + #[test] + fn ed25519_read_keypair_and_sign_with_keyid_hash_algorithms() { + let key = Ed25519PrivateKey::from_ed25519_with_keyid_hash_algorithms( + ed25519::PRIVATE_KEY, + python_tuf_compatibility_keyid_hash_algorithms(), + ) + .unwrap(); + let pub_key = PublicKey::from_ed25519_with_keyid_hash_algorithms( + ed25519::PUBLIC_KEY, + python_tuf_compatibility_keyid_hash_algorithms(), + ) + .unwrap(); + assert_eq!(key.public(), &pub_key); + + let role = MetadataPath::root(); + let msg = b"test"; + let sig = key.sign(msg).unwrap(); + assert_matches!(pub_key.verify(&role, msg, &sig), Ok(())); + + // Make sure we match what ring expects. + let ring_key = ring::signature::Ed25519KeyPair::from_pkcs8(ed25519::PK8_1).unwrap(); + assert_eq!(key.public().as_bytes(), ring_key.public_key().as_ref()); + assert_eq!(sig.value().as_bytes(), ring_key.sign(msg).as_ref()); + + // Make sure verification fails with the wrong key. + let bad_pub_key = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_2) + .unwrap() + .public() + .clone(); + + assert_matches!( + bad_pub_key.verify(&role, msg, &sig), + Err(Error::BadSignature(r)) + if r == role + ); + } + + #[test] + fn unknown_keytype_cannot_verify() { + let pub_key = PublicKey::new( + KeyType::Unknown("unknown-keytype".into()), + SignatureScheme::Unknown("unknown-scheme".into()), + None, + b"unknown-key".to_vec(), + ) + .unwrap(); + let role = MetadataPath::root(); + let msg = b"test"; + let sig = Signature { + key_id: KeyId("key-id".into()), + value: SignatureValue(b"sig-value".to_vec()), + }; + + assert_matches!( + pub_key.verify(&role, msg, &sig), + Err(Error::UnknownSignatureScheme(s)) + if s == "unknown-scheme" + ); + } + + #[test] + fn serde_key_id() { + let s = "4750eaf6878740780d6f97b12dbad079fb012bec88c78de2c380add56d3f51db"; + let jsn = json!(s); + let parsed: KeyId = serde_json::from_value(jsn.clone()).unwrap(); + assert_eq!(parsed, KeyId::from_str(s).unwrap()); + let encoded = serde_json::to_value(&parsed).unwrap(); + assert_eq!(encoded, jsn); + } + + #[test] + fn serde_key_type() { + let jsn = json!("ed25519"); + let parsed: KeyType = serde_json::from_value(jsn.clone()).unwrap(); + assert_eq!(parsed, KeyType::Ed25519); + + let encoded = serde_json::to_value(&parsed).unwrap(); + assert_eq!(encoded, jsn); + + let jsn = json!("unknown"); + let parsed: KeyType = serde_json::from_value(jsn).unwrap(); + assert_eq!(parsed, KeyType::Unknown("unknown".into())); + } + + #[test] + fn serde_signature_scheme() { + let jsn = json!("ed25519"); + let parsed: SignatureScheme = serde_json::from_value(jsn.clone()).unwrap(); + assert_eq!(parsed, SignatureScheme::Ed25519); + + let encoded = serde_json::to_value(&parsed).unwrap(); + assert_eq!(encoded, jsn); + + let jsn = json!("unknown"); + let parsed: SignatureScheme = serde_json::from_value(jsn).unwrap(); + assert_eq!(parsed, SignatureScheme::Unknown("unknown".into())); + } + + #[test] + fn serde_signature_value() { + let s = "4750eaf6878740780d6f97b12dbad079fb012bec88c78de2c380add56d3f51db"; + let jsn = json!(s); + let parsed: SignatureValue = serde_json::from_str(&format!("\"{}\"", s)).unwrap(); + assert_eq!( + parsed, + SignatureValue(HEXLOWER.decode(s.as_bytes()).unwrap()) + ); + let encoded = serde_json::to_value(&parsed).unwrap(); + assert_eq!(encoded, jsn); + } + + #[test] + fn serde_unknown_keytype_and_signature_scheme_public_key() { + let pub_key = PublicKey::new( + KeyType::Unknown("unknown-keytype".into()), + SignatureScheme::Unknown("unknown-scheme".into()), + None, + b"unknown-key".to_vec(), + ) + .unwrap(); + let encoded = serde_json::to_value(&pub_key).unwrap(); + let jsn = json!({ + "keytype": "unknown-keytype", + "scheme": "unknown-scheme", + "keyval": { + "public": "unknown-key", + } + }); + assert_eq!(encoded, jsn); + let decoded: PublicKey = serde_json::from_value(jsn).unwrap(); + assert_eq!(decoded, pub_key); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn serde_rsa_public_key() { + let der = rsa::SPKI_2048; + let pub_key = PublicKey::from_spki(der, SignatureScheme::RsaSsaPssSha256).unwrap(); + let encoded = serde_json::to_value(&pub_key).unwrap(); + let jsn = json!({ + "keytype": "rsa", + "scheme": "rsassa-pss-sha256", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": BASE64URL.encode(der), + } + }); + assert_eq!(encoded, jsn); + let decoded: PublicKey = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, pub_key); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn de_ser_rsa_public_key_with_keyid_hash_algo() { + let original = json!({ + "keytype": "rsa", + "scheme": "rsassa-pss-sha256", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": BASE64URL.encode(rsa::SPKI_2048), + } + }); + + let decoded: PublicKey = serde_json::from_value(original.clone()).unwrap(); + let encoded = serde_json::to_value(&decoded).unwrap(); + + assert_eq!(original, encoded); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn de_ser_rsa_public_key_without_keyid_hash_algo() { + let original = json!({ + "keytype": "rsa", + "scheme": "rsassa-pss-sha256", + "keyval": { + "public": BASE64URL.encode(rsa::SPKI_2048), + } + }); + + let decoded: PublicKey = serde_json::from_value(original.clone()).unwrap(); + let encoded = serde_json::to_value(&decoded).unwrap(); + + assert_eq!(original, encoded); + } + + #[test] + fn serde_ed25519_public_key() { + let pub_key = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_1) + .unwrap() + .public() + .clone(); + + let pub_key = PublicKey::from_ed25519_with_keyid_hash_algorithms( + pub_key.as_bytes().to_vec(), + python_tuf_compatibility_keyid_hash_algorithms(), + ) + .unwrap(); + let encoded = serde_json::to_value(&pub_key).unwrap(); + let jsn = json!({ + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": HEXLOWER.encode(pub_key.as_bytes()), + } + }); + assert_eq!(encoded, jsn); + let decoded: PublicKey = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, pub_key); + } + + #[test] + fn de_ser_ed25519_public_key_with_keyid_hash_algo() { + let pub_key = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_1) + .unwrap() + .public() + .clone(); + let pub_key = PublicKey::from_ed25519_with_keyid_hash_algorithms( + pub_key.as_bytes().to_vec(), + python_tuf_compatibility_keyid_hash_algorithms(), + ) + .unwrap(); + let original = json!({ + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": HEXLOWER.encode(pub_key.as_bytes()), + } + }); + + let encoded: PublicKey = serde_json::from_value(original.clone()).unwrap(); + let decoded = serde_json::to_value(&encoded).unwrap(); + + assert_eq!(original, decoded); + } + + #[test] + fn de_ser_ed25519_public_key_without_keyid_hash_algo() { + let pub_key = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_1) + .unwrap() + .public() + .clone(); + let pub_key = + PublicKey::from_ed25519_with_keyid_hash_algorithms(pub_key.as_bytes().to_vec(), None) + .unwrap(); + let original = json!({ + "keytype": "ed25519", + "scheme": "ed25519", + "keyval": { + "public": HEXLOWER.encode(pub_key.as_bytes()), + } + }); + + let encoded: PublicKey = serde_json::from_value(original.clone()).unwrap(); + let decoded = serde_json::to_value(&encoded).unwrap(); + + assert_eq!(original, decoded); + } + + #[test] + fn serde_signature() { + let key = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_1).unwrap(); + let msg = b"test"; + let sig = key.sign(msg).unwrap(); + let encoded = serde_json::to_value(&sig).unwrap(); + let jsn = json!({ + "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", + "sig": "fe4d13b2a73c033a1de7f5107b205fc7ba0e1566cb95b92349cae6aa453\ + 8956013bfe0f7bf977cb072bb65e8782b5f33a0573fe78816299a017ca5ba55\ + 9e390c", + }); + assert_eq!(encoded, jsn); + + let decoded: Signature = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, sig); + } + + #[test] + fn serde_signature_without_keyid_hash_algo() { + let key = + Ed25519PrivateKey::from_pkcs8_with_keyid_hash_algorithms(ed25519::PK8_1, None).unwrap(); + let msg = b"test"; + let sig = key.sign(msg).unwrap(); + let encoded = serde_json::to_value(&sig).unwrap(); + let jsn = json!({ + "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554", + "sig": "fe4d13b2a73c033a1de7f5107b205fc7ba0e1566cb95b92349cae6aa453\ + 8956013bfe0f7bf977cb072bb65e8782b5f33a0573fe78816299a017ca5ba55\ + 9e390c", + }); + assert_eq!(encoded, jsn); + + let decoded: Signature = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, sig); + } + + #[cfg(feature = "unstable_rsa")] + #[cfg(not(any(target_os = "fuchsia", windows)))] + #[test] + fn new_rsa_key() { + let bytes = RsaPrivateKey::pkcs8().unwrap(); + let _ = RsaPrivateKey::from_pkcs8(&bytes, SignatureScheme::RsaSsaPssSha256).unwrap(); + } + + #[test] + fn new_ed25519_key() { + let bytes = Ed25519PrivateKey::pkcs8().unwrap(); + let _ = Ed25519PrivateKey::from_pkcs8(&bytes).unwrap(); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn test_rsa_public_key_eq() { + let key256_1 = + PublicKey::from_spki(rsa::SPKI_2048, SignatureScheme::RsaSsaPssSha256).unwrap(); + let key256_2 = + PublicKey::from_spki(rsa::SPKI_2048, SignatureScheme::RsaSsaPssSha256).unwrap(); + let key512 = + PublicKey::from_spki(rsa::SPKI_2048, SignatureScheme::RsaSsaPssSha512).unwrap(); + + assert_eq!(key256_1, key256_2); + assert_ne!(key256_1, key512); + } + + #[test] + fn test_ed25519_public_key_eq() { + let key1 = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_1).unwrap(); + let key2 = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_2).unwrap(); + + assert_eq!(key1.public(), key1.public()); + assert_ne!(key1.public(), key2.public()); + } + + fn check_public_key_hash(key1: &PublicKey, key2: &PublicKey) { + use std::hash::{BuildHasher, Hash, Hasher}; + + let state = std::collections::hash_map::RandomState::new(); + let mut hasher1 = state.build_hasher(); + key1.hash(&mut hasher1); + + let mut hasher2 = state.build_hasher(); + key2.hash(&mut hasher2); + + assert_ne!(hasher1.finish(), hasher2.finish()); + } + + #[cfg(feature = "unstable_rsa")] + #[test] + fn test_rsa_public_key_hash() { + let key256 = + PublicKey::from_spki(rsa::SPKI_2048, SignatureScheme::RsaSsaPssSha256).unwrap(); + let key512 = + PublicKey::from_spki(rsa::SPKI_2048, SignatureScheme::RsaSsaPssSha512).unwrap(); + + check_public_key_hash(&key256, &key512); + } + + #[test] + fn test_ed25519_public_key_hash() { + let key1 = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_1).unwrap(); + let key2 = Ed25519PrivateKey::from_pkcs8(ed25519::PK8_2).unwrap(); + + check_public_key_hash(key1.public(), key2.public()); + } + + /// Recomputed ECDSA keyids must match the stamped keyids in the root. + #[test] + fn deserialize_ecdsa_keys_keyids_match() { + let root: serde_json::Value = serde_json::from_slice(ecdsa::ECDSA_ROOT).unwrap(); + let signed_keys = root["signed"]["keys"].as_object().unwrap(); + let stamped_keyids: Vec = root["signed"]["roles"]["root"]["keyids"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + + for (expected_keyid, key_json) in signed_keys { + let key: PublicKey = serde_json::from_value(key_json.clone()).unwrap(); + assert_eq!(key.typ(), &KeyType::Ecdsa); + assert_eq!(key.scheme(), &SignatureScheme::EcdsaSha2Nistp256); + assert_eq!( + key.key_id().0, + *expected_keyid, + "recomputed keyid {} does not match stamped keyid {}", + key.key_id().0, + expected_keyid + ); + assert!(stamped_keyids.contains(expected_keyid)); + } + } + + /// Each ECDSA signature on the test root must verify against its declared key. + #[test] + fn verify_ecdsa_signatures_against_canonical_signed_body() { + let root: serde_json::Value = serde_json::from_slice(ecdsa::ECDSA_ROOT).unwrap(); + let signed_keys = root["signed"]["keys"].as_object().unwrap(); + let signatures = root["signatures"].as_array().unwrap(); + let role = MetadataPath::root(); + let msg = ecdsa::ECDSA_ROOT_CANONICAL; + assert!(!signatures.is_empty(), "test root must have signatures"); + + for sig_json in signatures { + let sig: Signature = serde_json::from_value(sig_json.clone()).unwrap(); + let key_id_str = &sig.key_id().0; + let key_json = signed_keys + .get(key_id_str) + .unwrap_or_else(|| panic!("key {} not present in keys map", key_id_str)); + let key: PublicKey = serde_json::from_value(key_json.clone()).unwrap(); + key.verify(&role, msg, &sig).unwrap_or_else(|e| { + panic!("ECDSA signature {} did not verify: {:?}", key_id_str, e) + }); + } + } + + #[test] + fn ecdsa_signature_scheme_aliases() { + assert_eq!( + SignatureScheme::new("ecdsa-sha2-nistp256"), + SignatureScheme::EcdsaSha2Nistp256 + ); + assert_eq!( + SignatureScheme::new("ecdsa"), + SignatureScheme::EcdsaSha2Nistp256 + ); + assert_eq!( + SignatureScheme::EcdsaSha2Nistp256.as_str(), + "ecdsa-sha2-nistp256" + ); + } + + /// A bit-flipped signature must fail verification. + #[test] + fn verify_ecdsa_rejects_corrupted_signature() { + let root: serde_json::Value = serde_json::from_slice(ecdsa::ECDSA_ROOT).unwrap(); + let signed_keys = root["signed"]["keys"].as_object().unwrap(); + let sig_json = root["signatures"][0].clone(); + let sig: Signature = serde_json::from_value(sig_json).unwrap(); + let key_json = signed_keys.get(&sig.key_id().0).unwrap().clone(); + let key: PublicKey = serde_json::from_value(key_json).unwrap(); + + let mut bad_value = sig.value().0.clone(); + if let Some(last) = bad_value.last_mut() { + *last ^= 0x01; + } + let bad_sig = Signature { + key_id: sig.key_id().clone(), + value: SignatureValue(bad_value), + }; + + let result = key.verify(&MetadataPath::root(), ecdsa::ECDSA_ROOT_CANONICAL, &bad_sig); + assert_matches!(result, Err(Error::BadSignature(_))); + } +} diff --git a/vendor/tuf/src/database.rs b/vendor/tuf/src/database.rs new file mode 100644 index 0000000000..d187bc83b3 --- /dev/null +++ b/vendor/tuf/src/database.rs @@ -0,0 +1,1910 @@ +//! Components needed to verify TUF metadata and targets. + +use chrono::{offset::Utc, DateTime}; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; + +use crate::crypto::PublicKey; +use crate::error::Error; +use crate::interchange::DataInterchange; +use crate::metadata::{ + Delegations, Metadata, MetadataPath, MetadataVersion, RawSignedMetadata, RawSignedMetadataSet, + RootMetadata, SnapshotMetadata, TargetDescription, TargetPath, TargetsMetadata, + TimestampMetadata, +}; +use crate::verify::{self, Verified}; +use crate::Result; + +/// Contains trusted TUF metadata and can be used to verify other metadata and targets. +#[derive(Clone, Debug)] +pub struct Database { + trusted_root: Verified, + trusted_snapshot: Option>, + trusted_targets: Option>, + trusted_timestamp: Option>, + trusted_delegations: HashMap>, + interchange: PhantomData, +} + +impl Database { + /// Create a new [`Database`] struct from a set of trusted root keys that are used to verify + /// the signed metadata. The signed root metadata must be signed with at least a + /// `root_threshold` of the provided root_keys. It is not necessary for the root metadata to + /// contain these keys. + pub fn from_root_with_trusted_keys<'a, I>( + raw_root: &RawSignedMetadata, + root_threshold: u32, + root_keys: I, + ) -> Result + where + I: IntoIterator, + { + let verified_root = { + // Make sure the keys signed the root. + let new_root = verify::verify_signatures( + &MetadataPath::root(), + raw_root, + root_threshold, + root_keys, + )?; + + // Make sure the root signed itself. + verify::verify_signatures( + &MetadataPath::root(), + raw_root, + new_root.root().threshold(), + new_root.keys().iter().filter_map(|(k, v)| { + if new_root.root().key_ids().contains(k) { + Some(v) + } else { + None + } + }), + )? + }; + + Ok(Database { + trusted_root: verified_root, + trusted_snapshot: None, + trusted_targets: None, + trusted_timestamp: None, + trusted_delegations: HashMap::new(), + interchange: PhantomData, + }) + } + + /// Create a new [`Database`] struct from a piece of metadata that is assumed to be trusted. + /// + /// **WARNING**: This is trust-on-first-use (TOFU) and offers weaker security guarantees than + /// the related method [`Database::from_root_with_trusted_keys`] because this method needs to + /// deserialize `raw_root` before we have verified it has been signed properly. This exposes us + /// to potential parser exploits. This method should only be used if the metadata is loaded from + /// a trusted source. + pub fn from_trusted_root(raw_root: &RawSignedMetadata) -> Result { + let verified_root = { + // **WARNING**: By deserializing the metadata before verification, we are exposing us + // to parser exploits. + let unverified_root = raw_root.parse_untrusted()?.assume_valid()?; + + // Make sure the root signed itself. + verify::verify_signatures( + &MetadataPath::root(), + raw_root, + unverified_root.root().threshold(), + unverified_root.root_keys(), + )? + }; + + Ok(Database { + trusted_root: verified_root, + trusted_snapshot: None, + trusted_targets: None, + trusted_timestamp: None, + trusted_delegations: HashMap::new(), + interchange: PhantomData, + }) + } + + /// Create a new [`Database`] struct from a set of metadata that is assumed to be trusted. The + /// signed root metadata in the `metadata_set` must be signed with at least a `root_threshold` + /// of the provided root_keys. It is not necessary for the root metadata to contain these keys. + pub fn from_metadata_with_trusted_keys<'a, I>( + metadata_set: &RawSignedMetadataSet, + root_threshold: u32, + root_keys: I, + ) -> Result + where + I: IntoIterator, + { + Self::from_metadata_with_trusted_keys_and_start_time( + &Utc::now(), + metadata_set, + root_threshold, + root_keys, + ) + } + + /// Create a new [`Database`] struct from a set of metadata that is assumed to be trusted. The + /// signed root metadata in the `metadata_set` must be signed with at least a `root_threshold` + /// of the provided root_keys. It is not necessary for the root metadata to contain these keys. + pub fn from_metadata_with_trusted_keys_and_start_time<'a, I>( + start_time: &DateTime, + metadata_set: &RawSignedMetadataSet, + root_threshold: u32, + root_keys: I, + ) -> Result + where + I: IntoIterator, + { + let mut db = if let Some(root) = metadata_set.root() { + Database::from_root_with_trusted_keys(root, root_threshold, root_keys)? + } else { + return Err(Error::MetadataNotFound { + path: MetadataPath::root(), + version: MetadataVersion::None, + }); + }; + + db.update_metadata_after_root(start_time, metadata_set)?; + + Ok(db) + } + + /// Create a new [`Database`] struct from a set of metadata that is assumed to be trusted. + /// + /// **WARNING**: This is trust-on-first-use (TOFU) and offers weaker security guarantees than + /// the related method [`Database::from_metadata_with_trusted_keys`] because this method needs + /// to deserialize the root metadata from `metadata_set` before we have verified it has been + /// signed properly. This exposes us to potential parser exploits. This method should only be + /// used if the metadata is loaded from a trusted source. + pub fn from_trusted_metadata(metadata_set: &RawSignedMetadataSet) -> Result { + Self::from_trusted_metadata_with_start_time(metadata_set, &Utc::now()) + } + + /// Create a new [`Database`] struct from a set of metadata that is assumed to be trusted. + /// + /// **WARNING**: This is trust-on-first-use (TOFU) and offers weaker security guarantees than + /// the related method [`Database::from_metadata_with_trusted_keys`] because this method needs + /// to deserialize the root metadata from `metadata_set` before we have verified it has been + /// signed properly. This exposes us to potential parser exploits. This method should only be + /// used if the metadata is loaded from a trusted source. + pub fn from_trusted_metadata_with_start_time( + metadata_set: &RawSignedMetadataSet, + start_time: &DateTime, + ) -> Result { + let mut db = if let Some(root) = metadata_set.root() { + Database::from_trusted_root(root)? + } else { + return Err(Error::MetadataNotFound { + path: MetadataPath::root(), + version: MetadataVersion::None, + }); + }; + + db.update_metadata_after_root(start_time, metadata_set)?; + + Ok(db) + } + + /// An immutable reference to the root metadata. + pub fn trusted_root(&self) -> &Verified { + &self.trusted_root + } + + /// An immutable reference to the optional targets metadata. + pub fn trusted_targets(&self) -> Option<&Verified> { + self.trusted_targets.as_ref() + } + + /// An immutable reference to the optional snapshot metadata. + pub fn trusted_snapshot(&self) -> Option<&Verified> { + self.trusted_snapshot.as_ref() + } + + /// An immutable reference to the optional timestamp metadata. + pub fn trusted_timestamp(&self) -> Option<&Verified> { + self.trusted_timestamp.as_ref() + } + + /// An immutable reference to the delegated metadata. + pub fn trusted_delegations(&self) -> &HashMap> { + &self.trusted_delegations + } + + /// Verify and update metadata. Returns true if any of the metadata was updated. + pub fn update_metadata(&mut self, metadata: &RawSignedMetadataSet) -> Result { + self.update_metadata_with_start_time(metadata, &Utc::now()) + } + + /// Verify and update metadata. Returns true if any of the metadata was updated. + pub fn update_metadata_with_start_time( + &mut self, + metadata: &RawSignedMetadataSet, + start_time: &DateTime, + ) -> Result { + let updated = if let Some(root) = metadata.root() { + self.update_root(root)?; + true + } else { + false + }; + + if self.update_metadata_after_root(start_time, metadata)? { + Ok(true) + } else { + Ok(updated) + } + } + + fn update_metadata_after_root( + &mut self, + start_time: &DateTime, + metadata_set: &RawSignedMetadataSet, + ) -> Result { + let mut updated = false; + if let Some(timestamp) = metadata_set.timestamp() { + if self.update_timestamp(start_time, timestamp)?.is_some() { + updated = true; + } + } + + if let Some(snapshot) = metadata_set.snapshot() { + if self.update_snapshot(start_time, snapshot)? { + updated = true; + } + } + + if let Some(targets) = metadata_set.targets() { + if self.update_targets(start_time, targets)? { + updated = true; + } + } + + Ok(updated) + } + + /// Verify and update the root metadata. + pub fn update_root(&mut self, raw_root: &RawSignedMetadata) -> Result<()> { + let verified = { + let trusted_root = &self.trusted_root; + + ///////////////////////////////////////// + // TUF-1.0.5 §5.1.3: + // + // Check signatures. Version N+1 of the root metadata file MUST have been signed + // by: (1) a threshold of keys specified in the trusted root metadata file (version + // N), and (2) a threshold of keys specified in the new root metadata file being + // validated (version N+1). If version N+1 is not signed as required, discard it, + // abort the update cycle, and report the signature failure. On the next update + // cycle, begin at step 0 and version N of the root metadata file. Verify the + // trusted root signed the new root. + let new_root = verify::verify_signatures( + &MetadataPath::root(), + raw_root, + trusted_root.root().threshold(), + trusted_root.root_keys(), + )?; + + // Verify the new root signed itself. + let new_root = verify::verify_signatures( + &MetadataPath::root(), + raw_root, + new_root.root().threshold(), + new_root.root_keys(), + )?; + + ///////////////////////////////////////// + // TUF-1.0.5 §5.1.4: + // + // Check for a rollback attack. The version number of the trusted root metadata + // file (version N) must be less than or equal to the version number of the new + // root metadata file (version N+1). Effectively, this means checking that the + // version number signed in the new root metadata file is indeed N+1. If the + // version of the new root metadata file is less than the trusted metadata file, + // discard it, abort the update cycle, and report the rollback attack. On the next + // update cycle, begin at step 0 and version N of the root metadata file. + + let next_root_version = trusted_root.version().checked_add(1).ok_or_else(|| { + Error::MetadataVersionMustBeSmallerThanMaxU64(MetadataPath::root()) + })?; + + if new_root.version() != next_root_version { + return Err(Error::AttemptedMetadataRollBack { + role: MetadataPath::root(), + trusted_version: trusted_root.version(), + new_version: new_root.version(), + }); + } + + ///////////////////////////////////////// + // TUF-1.0.5 §5.1.5: + // + // Note that the expiration of the new (intermediate) root metadata file does not matter yet, because we will check for it in step 1.8. + + ///////////////////////////////////////// + // TUF-1.0.5 §5.1.8: + // + // Check for a freeze attack. The latest known time should be lower than the + // expiration timestamp in the trusted root metadata file (version N). If the + // trusted root metadata file has expired, abort the update cycle, report the + // potential freeze attack. On the next update cycle, begin at step 0 and version N + // of the root metadata file. + + // FIXME: root metadata expiration is performed in Client. We should restructure things + // such that it is performed here. + + new_root + }; + + ///////////////////////////////////////// + // TUF-1.0.5 §5.1.9: + // + // If the timestamp and / or snapshot keys have been rotated, then delete the + // trusted timestamp and snapshot metadata files. This is done in order to recover + // from fast-forward attacks after the repository has been compromised and + // recovered. A fast-forward attack happens when attackers arbitrarily increase the + // version numbers of: (1) the timestamp metadata, (2) the snapshot metadata, and / + // or (3) the targets, or a delegated targets, metadata file in the snapshot + // metadata. Please see the Mercury paper for more details. + + self.purge_metadata(); + + ///////////////////////////////////////// + // TUF-1.0.5 §5.1.6: + // + // 1.6. Set the trusted root metadata file to the new root metadata file. + + self.trusted_root = verified; + + Ok(()) + } + + /// Verify and update the timestamp metadata. + /// + /// Returns a reference to the parsed metadata if the metadata was newer. + pub fn update_timestamp( + &mut self, + start_time: &DateTime, + raw_timestamp: &RawSignedMetadata, + ) -> Result>> { + let verified = { + // FIXME(https://github.com/theupdateframework/specification/issues/113) Should we + // check if the root metadata is expired here? We do that in the other `Database::update_*` + // methods, but not here. + let trusted_root = &self.trusted_root; + + ///////////////////////////////////////// + // TUF-1.0.5 §5.2.1: + // + // Check signatures. The new timestamp metadata file must have been signed by a + // threshold of keys specified in the trusted root metadata file. If the new + // timestamp metadata file is not properly signed, discard it, abort the update + // cycle, and report the signature failure. + + let new_timestamp = verify::verify_signatures( + &MetadataPath::timestamp(), + raw_timestamp, + trusted_root.timestamp().threshold(), + trusted_root.timestamp_keys(), + )?; + + ///////////////////////////////////////// + // TUF-1.0.5 §5.2.2: Check for a rollback attack. + + ///////////////////////////////////////// + // TUF-1.0.5 §5.2.2.1: + // + // The version number of the trusted timestamp metadata file, if any, must be less + // than or equal to the version number of the new timestamp metadata file. If the + // new timestamp metadata file is older than the trusted timestamp metadata file, + // discard it, abort the update cycle, and report the potential rollback attack. + + if let Some(trusted_timestamp) = &self.trusted_timestamp { + match new_timestamp.version().cmp(&trusted_timestamp.version()) { + Ordering::Less => { + return Err(Error::AttemptedMetadataRollBack { + role: MetadataPath::timestamp(), + trusted_version: trusted_timestamp.version(), + new_version: new_timestamp.version(), + }); + } + Ordering::Equal => { + return Ok(None); + } + Ordering::Greater => {} + } + } + + ///////////////////////////////////////// + // TUF-1.0.5 §5.2.2.2: + // + // The version number of the snapshot metadata file in the trusted timestamp + // metadata file, if any, MUST be less than or equal to its version number in the + // new timestamp metadata file. If not, discard the new timestamp metadadata file, + // abort the update cycle, and report the failure. + + // FIXME(#294): Implement this section. + + ///////////////////////////////////////// + // FIXME(#297): forgetting the trusted snapshot here is not part of the spec. Do we need to + // do it? + + if let Some(trusted_snapshot) = &self.trusted_snapshot { + if trusted_snapshot.version() != new_timestamp.snapshot().version() { + self.trusted_snapshot = None; + } + } + + ///////////////////////////////////////// + // TUF-1.0.5 §5.2.3: + // + // Check for a freeze attack. The latest known time should be lower than the + // expiration timestamp in the new timestamp metadata file. If so, the new + // timestamp metadata file becomes the trusted timestamp metadata file. If the new + // timestamp metadata file has expired, discard it, abort the update cycle, and + // report the potential freeze attack. + + if new_timestamp.expires() <= start_time { + return Err(Error::ExpiredMetadata(MetadataPath::timestamp())); + } + + new_timestamp + }; + + self.trusted_timestamp = Some(verified); + Ok(self.trusted_timestamp.as_ref()) + } + + /// Verify and update the snapshot metadata. + pub fn update_snapshot( + &mut self, + start_time: &DateTime, + raw_snapshot: &RawSignedMetadata, + ) -> Result { + let verified = { + ///////////////////////////////////////// + // FIXME(https://github.com/theupdateframework/specification/issues/113) Checking if + // this metadata expired isn't part of the spec. Do we actually want to do this? + let trusted_root = self.trusted_root_unexpired(start_time)?; + let trusted_timestamp = self.trusted_timestamp_unexpired(start_time)?; + + if let Some(trusted_snapshot) = &self.trusted_snapshot { + match trusted_timestamp + .snapshot() + .version() + .cmp(&trusted_snapshot.version()) + { + Ordering::Less => { + return Err(Error::AttemptedMetadataRollBack { + role: MetadataPath::snapshot(), + trusted_version: trusted_snapshot.version(), + new_version: trusted_timestamp.snapshot().version(), + }); + } + Ordering::Equal => { + return Ok(false); + } + Ordering::Greater => {} + } + } + + ///////////////////////////////////////// + // TUF-1.0.5 §5.3.1: + // + // Check against timestamp metadata. The hashes and version number of the new + // snapshot metadata file MUST match the hashes (if any) and version number listed + // in the trusted timestamp metadata. If hashes and version do not match, discard + // the new snapshot metadata, abort the update cycle, and report the failure. + + // FIXME: rust-tuf checks the hash during download, but it would be better if we + // checked the hash here to make it easier to validate we've correctly implemented the + // spec. + + // NOTE(https://github.com/theupdateframework/specification/pull/112): Technically + // we're supposed to check the version before checking the signature, but we do it + // afterwards. That PR proposes formally moving the version check to after signature + // verification. + + ///////////////////////////////////////// + // TUF-1.0.5 §5.3.2: + // + // The new snapshot metadata file MUST have been signed by a threshold of keys + // specified in the trusted root metadata file. If the new snapshot metadata file + // is not signed as required, discard it, abort the update cycle, and report the + // signature failure. + + let new_snapshot = verify::verify_signatures( + &MetadataPath::snapshot(), + raw_snapshot, + trusted_root.snapshot().threshold(), + trusted_root.snapshot_keys(), + )?; + + ///////////////////////////////////////// + // FIXME(https://github.com/theupdateframework/specification/pull/112): Actually check + // the version. + + if new_snapshot.version() != trusted_timestamp.snapshot().version() { + return Err(Error::WrongMetadataVersion { + parent_role: MetadataPath::timestamp(), + child_role: MetadataPath::snapshot(), + expected_version: trusted_timestamp.snapshot().version(), + new_version: new_snapshot.version(), + }); + } + + ///////////////////////////////////////// + // TUF-1.0.5 §5.3.3: Check for a rollback attack. + + ///////////////////////////////////////// + // TUF-1.0.5 §5.3.3.1: + // + // The version number of the trusted snapshot metadata file, if any, MUST be less + // than or equal to the version number of the new snapshot metadata file. If the + // new snapshot metadata file is older than the trusted metadata file, discard it, + // abort the update cycle, and report the potential rollback attack. + + if let Some(trusted_snapshot) = &self.trusted_snapshot { + if new_snapshot.version() < trusted_snapshot.version() { + return Err(Error::AttemptedMetadataRollBack { + role: MetadataPath::snapshot(), + trusted_version: trusted_snapshot.version(), + new_version: new_snapshot.version(), + }); + } + } + + ///////////////////////////////////////// + // TUF-1.0.5 §5.3.3.2: + // + // The version number of the targets metadata file, and all delegated targets + // metadata files (if any), in the trusted snapshot metadata file, if any, MUST be + // less than or equal to its version number in the new snapshot metadata file. + // Furthermore, any targets metadata filename that was listed in the trusted + // snapshot metadata file, if any, MUST continue to be listed in the new snapshot + // metadata file. If any of these conditions are not met, discard the new snapshot + // metadadata file, abort the update cycle, and report the failure. + + // FIXME(#295): Implement this section. + + ///////////////////////////////////////// + // TUF-1.0.5 §5.3.4: + // + // Check for a freeze attack. The latest known time should be lower than the + // expiration timestamp in the new snapshot metadata file. If so, the new snapshot + // metadata file becomes the trusted snapshot metadata file. If the new snapshot + // metadata file is expired, discard it, abort the update cycle, and report the + // potential freeze attack. + + ///////////////////////////////////////// + // FIXME(#297): Verify why we don't check expiration here: + // Note: this doesn't check the expiration because we need to be able to update it + // regardless so we can prevent rollback attacks againsts targets/delegations. + + new_snapshot + }; + + // FIXME(#297): purging targets is not part of the spec. Do we need to do it? + if self + .trusted_targets + .as_ref() + .map(|s| s.version()) + .unwrap_or(0) + != verified + .meta() + .get(&MetadataPath::targets()) + .map(|m| m.version()) + .unwrap_or(0) + { + self.trusted_targets = None; + } + + self.trusted_snapshot = Some(verified); + + // FIXME(#297): purging delegates is not part of the spec. Do we need to do it? + self.purge_delegations(); + + Ok(true) + } + + fn purge_delegations(&mut self) { + let purge = { + let trusted_snapshot = match self.trusted_snapshot() { + Some(s) => s, + None => return, + }; + let mut purge = HashSet::new(); + for (role, trusted_definition) in trusted_snapshot.meta().iter() { + let trusted_delegation = match self.trusted_delegations.get(role) { + Some(d) => d, + None => continue, + }; + + if trusted_delegation.version() > trusted_definition.version() { + let _ = purge.insert(role.clone()); + continue; + } + } + + purge + }; + + for role in &purge { + let _ = self.trusted_delegations.remove(role); + } + } + + /// Verify and update the targets metadata. + pub fn update_targets( + &mut self, + start_time: &DateTime, + raw_targets: &RawSignedMetadata, + ) -> Result { + let verified = { + // FIXME(https://github.com/theupdateframework/specification/issues/113) Checking if + // this metadata expired isn't part of the spec. Do we actually want to do this? + let trusted_root = self.trusted_root_unexpired(start_time)?; + let trusted_targets_version = self.trusted_targets.as_ref().map(|t| t.version()); + + self.verify_target_or_delegated_target( + start_time, + &MetadataPath::targets(), + raw_targets, + trusted_root.targets().threshold(), + trusted_root.targets_keys(), + trusted_targets_version, + )? + }; + + if let Some(verified) = verified { + self.trusted_targets = Some(verified); + Ok(true) + } else { + Ok(false) + } + } + + /// Verify and update a delegation metadata. + pub fn update_delegated_targets( + &mut self, + start_time: &DateTime, + parent_role: &MetadataPath, + role: &MetadataPath, + raw_delegated_targets: &RawSignedMetadata, + ) -> Result { + let verified = { + // FIXME(https://github.com/theupdateframework/specification/issues/113) Checking if + // this metadata expired isn't part of the spec. Do we actually want to do this? + let _ = self.trusted_root_unexpired(start_time)?; + let _ = self.trusted_snapshot_unexpired(start_time)?; + let trusted_targets = self.trusted_targets_unexpired(start_time)?; + + if trusted_targets.delegations().is_empty() { + return Err(Error::UnauthorizedDelegation { + parent_role: parent_role.clone(), + child_role: role.clone(), + }); + }; + + let (threshold, keys) = self + .find_delegation_threshold_and_keys(parent_role, role)? + .ok_or_else(|| Error::UnauthorizedDelegation { + parent_role: parent_role.clone(), + child_role: role.clone(), + })?; + + let trusted_delegated_targets_version = + self.trusted_delegations.get(role).map(|t| t.version()); + + self.verify_target_or_delegated_target( + start_time, + role, + raw_delegated_targets, + threshold, + keys.into_iter(), + trusted_delegated_targets_version, + )? + }; + + if let Some(verified) = verified { + let _ = self.trusted_delegations.insert(role.clone(), verified); + Ok(true) + } else { + Ok(false) + } + } + + fn verify_target_or_delegated_target<'a>( + &self, + start_time: &DateTime, + role: &MetadataPath, + raw_targets: &RawSignedMetadata, + trusted_targets_threshold: u32, + trusted_targets_keys: impl Iterator, + trusted_targets_version: Option, + ) -> Result>> { + // FIXME(https://github.com/theupdateframework/specification/issues/113) Checking if + // this metadata expired isn't part of the spec. Do we actually want to do this? + let trusted_snapshot = self.trusted_snapshot_unexpired(start_time)?; + + let trusted_targets_description = + trusted_snapshot + .meta() + .get(role) + .ok_or_else(|| Error::MissingMetadataDescription { + parent_role: MetadataPath::snapshot(), + child_role: role.clone(), + })?; + + ///////////////////////////////////////// + // TUF-1.0.5 §5.4.1: + // + // Check against snapshot metadata. The hashes and version number of the new + // targets metadata file MUST match the hashes (if any) and version number listed + // in the trusted snapshot metadata. This is done, in part, to prevent a + // mix-and-match attack by man-in-the-middle attackers. If the new targets metadata + // file does not match, discard it, abort the update cycle, and report the failure. + + // FIXME: rust-tuf checks the hash during download, but it would be better if we + // checked the hash here to make it easier to validate we've correctly implemented the + // spec. + + // NOTE(https://github.com/theupdateframework/specification/pull/112): Technically + // we're supposed to check the version before checking the signature, but we do it + // afterwards. That PR proposes formally moving the version check to after signature + // verification. + + ///////////////////////////////////////// + // TUF-1.0.5 §5.4.2: + // + // Check for an arbitrary software attack. The new targets metadata file MUST have + // been signed by a threshold of keys specified in the trusted root metadata file. + // If the new targets metadata file is not signed as required, discard it, abort + // the update cycle, and report the failure. + + let new_targets = verify::verify_signatures( + role, + raw_targets, + trusted_targets_threshold, + trusted_targets_keys, + )?; + + ///////////////////////////////////////// + // FIXME(https://github.com/theupdateframework/specification/pull/112): Actually check + // the version. + + // FIXME(#295): TUF-1.0.5 §5.3.3.2 says this check should be done when updating the + // snapshot, not here. + if new_targets.version() != trusted_targets_description.version() { + return Err(Error::WrongMetadataVersion { + parent_role: MetadataPath::snapshot(), + child_role: role.clone(), + expected_version: trusted_targets_description.version(), + new_version: new_targets.version(), + }); + } + + if let Some(trusted_targets_version) = trusted_targets_version { + match new_targets.version().cmp(&trusted_targets_version) { + Ordering::Less => { + return Err(Error::AttemptedMetadataRollBack { + role: role.clone(), + trusted_version: trusted_targets_version, + new_version: new_targets.version(), + }); + } + Ordering::Equal => { + return Ok(None); + } + Ordering::Greater => {} + } + } + + ///////////////////////////////////////// + // TUF-1.0.5 §5.4.3: + // + // Check for a freeze attack. The latest known time should be lower than the + // expiration timestamp in the new targets metadata file. If so, the new targets + // metadata file becomes the trusted targets metadata file. If the new targets + // metadata file is expired, discard it, abort the update cycle, and report the + // potential freeze attack. + + if new_targets.expires() <= start_time { + return Err(Error::ExpiredMetadata(role.clone())); + } + + Ok(Some(new_targets)) + } + + /// Find the signing keys and metadata for the delegation given by `role`, as seen from the + /// point of view of `parent_role`. + fn find_delegation_threshold_and_keys( + &self, + parent_role: &MetadataPath, + role: &MetadataPath, + ) -> Result)>> { + // Find the parent TargetsMetadata that is expected to refer to `role`. + let trusted_parent = if parent_role == &MetadataPath::targets() { + if let Some(trusted_targets) = self.trusted_targets() { + trusted_targets + } else { + return Err(Error::MetadataNotFound { + path: parent_role.clone(), + version: MetadataVersion::None, + }); + } + } else if let Some(trusted_parent) = self.trusted_delegations.get(parent_role) { + trusted_parent + } else { + return Err(Error::MetadataNotFound { + path: parent_role.clone(), + version: MetadataVersion::None, + }); + }; + + // Only consider targets metadata that define delegations. + let trusted_delegations = trusted_parent.delegations(); + + for trusted_delegation in trusted_delegations.roles() { + if trusted_delegation.name() != role { + continue; + } + + // Filter the delegations keys to just the ones for this delegation. + let authorized_keys = trusted_delegations + .keys() + .iter() + .filter_map(|(k, v)| { + if trusted_delegation.key_ids().contains(k) { + Some(v) + } else { + None + } + }) + .collect(); + + return Ok(Some((trusted_delegation.threshold(), authorized_keys))); + } + + Ok(None) + } + + /// Get a reference to the description needed to verify the target defined by the given + /// `TargetPath`. Returns an `Error` if the target is not defined in the trusted + /// metadata. This may mean the target exists somewhere in the metadata, but the chain of trust + /// to that target may be invalid or incomplete. + pub fn target_description(&self, target_path: &TargetPath) -> Result { + self.target_description_with_start_time(&Utc::now(), target_path) + } + + /// Get a reference to the description needed to verify the target defined by the given + /// `TargetPath`. Returns an `Error` if the target is not defined in the trusted + /// metadata. This may mean the target exists somewhere in the metadata, but the chain of trust + /// to that target may be invalid or incomplete. + pub fn target_description_with_start_time( + &self, + start_time: &DateTime, + target_path: &TargetPath, + ) -> Result { + let _ = self.trusted_root_unexpired(start_time)?; + let _ = self.trusted_snapshot_unexpired(start_time)?; + let targets = self.trusted_targets_unexpired(start_time)?; + + if let Some(d) = targets.targets().get(target_path) { + return Ok(d.clone()); + } + + fn lookup<'a, D: DataInterchange>( + start_time: &DateTime, + tuf: &'a Database, + default_terminate: bool, + current_depth: u32, + target_path: &TargetPath, + delegations: &'a Delegations, + parents: &[HashSet], + visited: &mut HashSet<&'a MetadataPath>, + ) -> (bool, Option) { + for delegation in delegations.roles() { + if visited.contains(delegation.name()) { + return (delegation.terminating(), None); + } + let _ = visited.insert(delegation.name()); + + let mut new_parents = parents.to_owned(); + new_parents.push(delegation.paths().clone()); + + if current_depth > 0 && !target_path.matches_chain(parents) { + return (delegation.terminating(), None); + } + + let trusted_delegation = match tuf.trusted_delegations.get(delegation.name()) { + Some(trusted_delegation) => trusted_delegation, + None => return (delegation.terminating(), None), + }; + + if trusted_delegation.expires() <= start_time { + return (delegation.terminating(), None); + } + + if let Some(target) = trusted_delegation.targets().get(target_path) { + return (delegation.terminating(), Some(target.clone())); + } + + let trusted_child_delegations = trusted_delegation.delegations(); + + // We only need to check the child delegations if it delegates to any child roles. + if !trusted_child_delegations.roles().is_empty() { + let mut new_parents = parents.to_vec(); + new_parents.push(delegation.paths().clone()); + let (term, res) = lookup( + start_time, + tuf, + delegation.terminating(), + current_depth + 1, + target_path, + trusted_child_delegations, + &new_parents, + visited, + ); + if term { + return (true, res); + } else if res.is_some() { + return (term, res); + } + } + } + (default_terminate, None) + } + + let delegations = targets.delegations(); + if delegations.roles().is_empty() { + Err(Error::TargetNotFound(target_path.clone())) + } else { + let mut visited = HashSet::new(); + lookup( + start_time, + self, + false, + 0, + target_path, + delegations, + &[], + &mut visited, + ) + .1 + .ok_or_else(|| Error::TargetNotFound(target_path.clone())) + } + } + + /// Clear all trusted non-root metadata (snapshot, targets, timestamp, and + /// delegated targets) while preserving the currently trusted root — including + /// any newer root version reached via [`Database::update_root`] chaining. + /// + /// This is the same operation performed internally after a successful root + /// rotation per TUF-1.0.5 §5.1.9 to recover from fast-forward attacks. It is + /// exposed so callers can drop potentially-poisoned derived metadata (e.g. + /// after a mid-update failure) without discarding the advanced trusted root + /// and being forced to restart chaining from an embedded/bundled root. + /// + /// Only in-memory verified state on the [`Database`] is affected; any local + /// or remote repository caches held by a [`crate::client::Client`] are not + /// touched by this method. + pub fn purge_metadata(&mut self) { + self.trusted_snapshot = None; + self.trusted_targets = None; + self.trusted_timestamp = None; + self.trusted_delegations.clear(); + } + + fn trusted_root_unexpired(&self, start_time: &DateTime) -> Result<&RootMetadata> { + let trusted_root = &self.trusted_root; + if trusted_root.expires() <= start_time { + return Err(Error::ExpiredMetadata(MetadataPath::root())); + } + Ok(trusted_root) + } + + fn trusted_timestamp_unexpired( + &self, + start_time: &DateTime, + ) -> Result<&TimestampMetadata> { + match self.trusted_timestamp { + Some(ref trusted_timestamp) => { + if trusted_timestamp.expires() <= start_time { + return Err(Error::ExpiredMetadata(MetadataPath::timestamp())); + } + Ok(trusted_timestamp) + } + None => Err(Error::MetadataNotFound { + path: MetadataPath::timestamp(), + version: MetadataVersion::None, + }), + } + } + + fn trusted_snapshot_unexpired(&self, start_time: &DateTime) -> Result<&SnapshotMetadata> { + match self.trusted_snapshot { + Some(ref trusted_snapshot) => { + if trusted_snapshot.expires() <= start_time { + return Err(Error::ExpiredMetadata(MetadataPath::snapshot())); + } + Ok(trusted_snapshot) + } + None => Err(Error::MetadataNotFound { + path: MetadataPath::snapshot(), + version: MetadataVersion::None, + }), + } + } + + fn trusted_targets_unexpired(&self, start_time: &DateTime) -> Result<&TargetsMetadata> { + match self.trusted_targets { + Some(ref trusted_targets) => { + if trusted_targets.expires() <= start_time { + return Err(Error::ExpiredMetadata(MetadataPath::targets())); + } + Ok(trusted_targets) + } + None => Err(Error::MetadataNotFound { + path: MetadataPath::targets(), + version: MetadataVersion::None, + }), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::crypto::{Ed25519PrivateKey, HashAlgorithm, PrivateKey}; + use crate::interchange::Json; + use crate::metadata::{ + RawSignedMetadataSetBuilder, RootMetadataBuilder, SnapshotMetadataBuilder, + TargetsMetadataBuilder, TimestampMetadataBuilder, + }; + use assert_matches::assert_matches; + use lazy_static::lazy_static; + use std::iter::once; + + lazy_static! { + static ref KEYS: Vec = { + 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() + }; + } + + #[test] + fn root_trusted_keys_success() { + let root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[0].public().clone()) + .timestamp_key(KEYS[0].public().clone()) + .signed::(&KEYS[0]) + .unwrap(); + let raw_root = root.to_raw().unwrap(); + + assert_matches!( + Database::from_root_with_trusted_keys(&raw_root, 1, once(KEYS[0].public())), + Ok(_) + ); + } + + #[test] + fn root_trusted_keys_failure() { + let root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[0].public().clone()) + .timestamp_key(KEYS[0].public().clone()) + .signed::(&KEYS[0]) + .unwrap(); + let raw_root = root.to_raw().unwrap(); + + assert_matches!( + Database::from_root_with_trusted_keys(&raw_root, 1, once(KEYS[1].public())), + Err(Error::MetadataMissingSignatures { + role, + number_of_valid_signatures: 0, + threshold: 1, + }) + if role == MetadataPath::root() + ); + } + + #[test] + fn from_trusted_metadata_success() { + let root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[0].public().clone()) + .timestamp_key(KEYS[0].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata = RawSignedMetadataSetBuilder::new().root(root).build(); + + assert_matches!(Database::from_trusted_metadata(&metadata), Ok(_)); + } + + #[test] + fn from_trusted_metadata_failure() { + let root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[0].public().clone()) + .timestamp_key(KEYS[0].public().clone()) + .signed::(&KEYS[1]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata = RawSignedMetadataSetBuilder::new().root(root).build(); + + assert_matches!( + Database::from_trusted_metadata(&metadata), + Err(Error::MetadataMissingSignatures { + role, + number_of_valid_signatures: 0, + threshold: 1, + }) + if role == MetadataPath::root() + ); + } + + #[test] + fn from_metadata_with_trusted_keys_success() { + let root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[0].public().clone()) + .timestamp_key(KEYS[0].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata = RawSignedMetadataSetBuilder::new().root(root).build(); + + assert_matches!( + Database::from_metadata_with_trusted_keys(&metadata, 1, once(KEYS[0].public())), + Ok(_) + ); + } + + #[test] + fn from_metadata_with_trusted_keys_failure() { + let root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[0].public().clone()) + .timestamp_key(KEYS[0].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata = RawSignedMetadataSetBuilder::new().root(root).build(); + + assert_matches!( + Database::from_metadata_with_trusted_keys(&metadata, 1, once(KEYS[1].public())), + Err(Error::MetadataMissingSignatures { + role, + number_of_valid_signatures: 0, + threshold: 1, + }) + if role == MetadataPath::root() + ); + } + + #[test] + fn good_root_rotation() { + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[0].public().clone()) + .timestamp_key(KEYS[0].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let mut root = RootMetadataBuilder::new() + .version(2) + .root_key(KEYS[1].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[1].public().clone()) + .timestamp_key(KEYS[1].public().clone()) + .signed::(&KEYS[1]) + .unwrap(); + + // add the original key's signature to make it cross signed + root.add_signature(&KEYS[0]).unwrap(); + let raw_root = root.to_raw().unwrap(); + + assert_matches!(tuf.update_root(&raw_root), Ok(())); + + // second update with the same metadata should fail. + assert_matches!( + tuf.update_root(&raw_root), + Err(Error::AttemptedMetadataRollBack { role, trusted_version: 2, new_version: 2 }) + if role == MetadataPath::root() + ); + } + + #[test] + fn no_cross_sign_root_rotation() { + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[0].public().clone()) + .timestamp_key(KEYS[0].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[1].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[1].public().clone()) + .timestamp_key(KEYS[1].public().clone()) + .signed::(&KEYS[1]) + .unwrap() + .to_raw() + .unwrap(); + + assert!(tuf.update_root(&raw_root).is_err()); + } + + #[test] + fn good_timestamp_update() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[1].public().clone()) + .timestamp_key(KEYS[1].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let snapshot = SnapshotMetadataBuilder::new() + .signed::(&KEYS[1]) + .unwrap(); + + let timestamp = + TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[1]) + .unwrap(); + let raw_timestamp = timestamp.to_raw().unwrap(); + + assert_matches!( + tuf.update_timestamp(&now, &raw_timestamp), + Ok(Some(_parsed_timestamp)) + ); + + // second update should do nothing + assert_matches!(tuf.update_timestamp(&now, &raw_timestamp), Ok(None)) + } + + #[test] + fn bad_timestamp_update_wrong_key() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[1].public().clone()) + .timestamp_key(KEYS[1].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let snapshot = SnapshotMetadataBuilder::new() + .signed::(&KEYS[1]) + .unwrap(); + + let raw_timestamp = + TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256]) + .unwrap() + // sign it with the root key + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + assert!(tuf.update_timestamp(&now, &raw_timestamp).is_err()) + } + + #[test] + fn good_snapshot_update() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[2].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let snapshot = SnapshotMetadataBuilder::new().signed(&KEYS[1]).unwrap(); + let raw_snapshot = snapshot.to_raw().unwrap(); + + let raw_timestamp = + TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp).unwrap(); + + assert_matches!(tuf.update_snapshot(&now, &raw_snapshot), Ok(true)); + + // second update should do nothing + assert_matches!(tuf.update_snapshot(&now, &raw_snapshot), Ok(false)); + } + + #[test] + fn bad_snapshot_update_wrong_key() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[2].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let snapshot = SnapshotMetadataBuilder::new() + .signed::(&KEYS[2]) + .unwrap(); + let raw_snapshot = snapshot.to_raw().unwrap(); + + let raw_timestamp = + TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256]) + .unwrap() + // sign it with the targets key + .signed::(&KEYS[2]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp).unwrap(); + + assert!(tuf.update_snapshot(&now, &raw_snapshot).is_err()); + } + + #[test] + fn bad_snapshot_update_wrong_version() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[2].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let snapshot = SnapshotMetadataBuilder::new() + .version(2) + .signed::(&KEYS[2]) + .unwrap(); + + let raw_timestamp = + TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp).unwrap(); + + let raw_snapshot = SnapshotMetadataBuilder::new() + .version(1) + .signed::(&KEYS[1]) + .unwrap() + .to_raw() + .unwrap(); + + assert!(tuf.update_snapshot(&now, &raw_snapshot).is_err()); + } + + #[test] + fn good_targets_update() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let signed_targets = TargetsMetadataBuilder::new() + .signed::(&KEYS[2]) + .unwrap(); + let raw_targets = signed_targets.to_raw().unwrap(); + + let snapshot = SnapshotMetadataBuilder::new() + .insert_metadata(&signed_targets, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[1]) + .unwrap(); + let raw_snapshot = snapshot.to_raw().unwrap(); + + let raw_timestamp = + TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp).unwrap(); + tuf.update_snapshot(&now, &raw_snapshot).unwrap(); + + assert_matches!(tuf.update_targets(&now, &raw_targets), Ok(true)); + + // second update should do nothing + assert_matches!(tuf.update_targets(&now, &raw_targets), Ok(false)); + } + + #[test] + fn bad_targets_update_wrong_key() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let signed_targets = TargetsMetadataBuilder::new() + // sign it with the timestamp key + .signed::(&KEYS[3]) + .unwrap(); + let raw_targets = signed_targets.to_raw().unwrap(); + + let snapshot = SnapshotMetadataBuilder::new() + .insert_metadata(&signed_targets, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[1]) + .unwrap(); + let raw_snapshot = snapshot.to_raw().unwrap(); + + let raw_timestamp = + TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp).unwrap(); + tuf.update_snapshot(&now, &raw_snapshot).unwrap(); + + assert!(tuf.update_targets(&now, &raw_targets).is_err()); + } + + #[test] + fn bad_targets_update_wrong_version() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let signed_targets = TargetsMetadataBuilder::new() + .version(2) + .signed::(&KEYS[2]) + .unwrap(); + + let snapshot = SnapshotMetadataBuilder::new() + .insert_metadata(&signed_targets, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[1]) + .unwrap(); + let raw_snapshot = snapshot.to_raw().unwrap(); + + let raw_timestamp = + TimestampMetadataBuilder::from_snapshot(&snapshot, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp).unwrap(); + tuf.update_snapshot(&now, &raw_snapshot).unwrap(); + + let raw_targets = TargetsMetadataBuilder::new() + .version(1) + .signed::(&KEYS[2]) + .unwrap() + .to_raw() + .unwrap(); + + assert!(tuf.update_targets(&now, &raw_targets).is_err()); + } + + #[test] + fn test_update_metadata_succeeds_with_good_metadata() { + let raw_root1 = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .targets_key(KEYS[1].public().clone()) + .snapshot_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let signed_targets1 = TargetsMetadataBuilder::new() + .signed::(&KEYS[1]) + .unwrap(); + let raw_targets1 = signed_targets1.to_raw().unwrap(); + + let snapshot1 = SnapshotMetadataBuilder::new() + .insert_metadata(&signed_targets1, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap(); + let raw_snapshot1 = snapshot1.to_raw().unwrap(); + + let raw_timestamp1 = + TimestampMetadataBuilder::from_snapshot(&snapshot1, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata1 = RawSignedMetadataSetBuilder::new() + .root(raw_root1) + .targets(raw_targets1) + .snapshot(raw_snapshot1) + .timestamp(raw_timestamp1) + .build(); + + let mut tuf = Database::from_trusted_metadata(&metadata1).unwrap(); + + let raw_root2 = RootMetadataBuilder::new() + .version(2) + .root_key(KEYS[0].public().clone()) + .targets_key(KEYS[1].public().clone()) + .snapshot_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let signed_targets2 = TargetsMetadataBuilder::new() + .version(2) + .signed::(&KEYS[1]) + .unwrap(); + let raw_targets2 = signed_targets2.to_raw().unwrap(); + + let snapshot2 = SnapshotMetadataBuilder::new() + .version(2) + .insert_metadata(&signed_targets2, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap(); + let raw_snapshot2 = snapshot2.to_raw().unwrap(); + + let raw_timestamp2 = + TimestampMetadataBuilder::from_snapshot(&snapshot2, &[HashAlgorithm::Sha256]) + .unwrap() + .version(2) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata2 = RawSignedMetadataSetBuilder::new() + .root(raw_root2) + .targets(raw_targets2) + .snapshot(raw_snapshot2) + .timestamp(raw_timestamp2) + .build(); + + assert_matches!(tuf.update_metadata(&metadata2), Ok(true)); + } + + #[test] + fn test_purge_metadata_preserves_trusted_root() { + // Build v1 metadata and load it as trusted. + let raw_root1 = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .targets_key(KEYS[1].public().clone()) + .snapshot_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let signed_targets1 = TargetsMetadataBuilder::new() + .signed::(&KEYS[1]) + .unwrap(); + let raw_targets1 = signed_targets1.to_raw().unwrap(); + + let snapshot1 = SnapshotMetadataBuilder::new() + .insert_metadata(&signed_targets1, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap(); + let raw_snapshot1 = snapshot1.to_raw().unwrap(); + + let raw_timestamp1 = + TimestampMetadataBuilder::from_snapshot(&snapshot1, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata1 = RawSignedMetadataSetBuilder::new() + .root(raw_root1) + .targets(raw_targets1) + .snapshot(raw_snapshot1) + .timestamp(raw_timestamp1) + .build(); + + let mut tuf = Database::from_trusted_metadata(&metadata1).unwrap(); + + // Rotate to root v2 via update_metadata so trusted_root is advanced + // through the chained-rotation code path (not just replaced by TOFU). + let raw_root2 = RootMetadataBuilder::new() + .version(2) + .root_key(KEYS[0].public().clone()) + .targets_key(KEYS[1].public().clone()) + .snapshot_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let signed_targets2 = TargetsMetadataBuilder::new() + .version(2) + .signed::(&KEYS[1]) + .unwrap(); + let raw_targets2 = signed_targets2.to_raw().unwrap(); + + let snapshot2 = SnapshotMetadataBuilder::new() + .version(2) + .insert_metadata(&signed_targets2, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap(); + let raw_snapshot2 = snapshot2.to_raw().unwrap(); + + let raw_timestamp2 = + TimestampMetadataBuilder::from_snapshot(&snapshot2, &[HashAlgorithm::Sha256]) + .unwrap() + .version(2) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata2 = RawSignedMetadataSetBuilder::new() + .root(raw_root2) + .targets(raw_targets2) + .snapshot(raw_snapshot2) + .timestamp(raw_timestamp2) + .build(); + + assert_matches!(tuf.update_metadata(&metadata2), Ok(true)); + + // Sanity: everything is populated and root is at v2. + assert_eq!(tuf.trusted_root().version(), 2); + assert!(tuf.trusted_snapshot().is_some()); + assert!(tuf.trusted_targets().is_some()); + assert!(tuf.trusted_timestamp().is_some()); + + // Purge: root stays at v2, derived metadata is cleared. + tuf.purge_metadata(); + + assert_eq!(tuf.trusted_root().version(), 2); + assert!(tuf.trusted_snapshot().is_none()); + assert!(tuf.trusted_targets().is_none()); + assert!(tuf.trusted_timestamp().is_none()); + assert!(tuf.trusted_delegations().is_empty()); + + // A subsequent update must succeed against the retained trusted root: + // resubmit v2 timestamp/snapshot/targets without the root (which would + // otherwise be rejected as a rollback since we're already at v2). + let signed_targets2_replay = TargetsMetadataBuilder::new() + .version(2) + .signed::(&KEYS[1]) + .unwrap(); + let raw_targets2_replay = signed_targets2_replay.to_raw().unwrap(); + + let snapshot2_replay = SnapshotMetadataBuilder::new() + .version(2) + .insert_metadata(&signed_targets2_replay, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap(); + let raw_snapshot2_replay = snapshot2_replay.to_raw().unwrap(); + + let raw_timestamp2_replay = + TimestampMetadataBuilder::from_snapshot(&snapshot2_replay, &[HashAlgorithm::Sha256]) + .unwrap() + .version(2) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata2_replay = RawSignedMetadataSetBuilder::new() + .targets(raw_targets2_replay) + .snapshot(raw_snapshot2_replay) + .timestamp(raw_timestamp2_replay) + .build(); + + assert_matches!(tuf.update_metadata(&metadata2_replay), Ok(true)); + assert_eq!(tuf.trusted_root().version(), 2); + assert!(tuf.trusted_snapshot().is_some()); + assert!(tuf.trusted_targets().is_some()); + assert!(tuf.trusted_timestamp().is_some()); + } + + #[test] + fn test_update_metadata_fails_with_bad_metadata() { + let raw_root1 = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .targets_key(KEYS[1].public().clone()) + .snapshot_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let signed_targets1 = TargetsMetadataBuilder::new() + .signed::(&KEYS[1]) + .unwrap(); + let raw_targets1 = signed_targets1.to_raw().unwrap(); + + let snapshot1 = SnapshotMetadataBuilder::new() + .insert_metadata(&signed_targets1, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap(); + let raw_snapshot1 = snapshot1.to_raw().unwrap(); + + let raw_timestamp1 = + TimestampMetadataBuilder::from_snapshot(&snapshot1, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata1 = RawSignedMetadataSetBuilder::new() + .root(raw_root1) + .targets(raw_targets1) + .snapshot(raw_snapshot1) + .timestamp(raw_timestamp1) + .build(); + + let mut tuf = Database::from_trusted_metadata(&metadata1).unwrap(); + + let raw_root2 = RootMetadataBuilder::new() + .version(2) + .root_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .snapshot_key(KEYS[3].public().clone()) + .timestamp_key(KEYS[4].public().clone()) + .signed::(&KEYS[1]) + .unwrap() + .to_raw() + .unwrap(); + + let signed_targets2 = TargetsMetadataBuilder::new() + .version(2) + .signed::(&KEYS[1]) + .unwrap(); + let raw_targets2 = signed_targets2.to_raw().unwrap(); + + let snapshot2 = SnapshotMetadataBuilder::new() + .version(2) + .insert_metadata(&signed_targets2, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[2]) + .unwrap(); + let raw_snapshot2 = snapshot2.to_raw().unwrap(); + + let raw_timestamp2 = + TimestampMetadataBuilder::from_snapshot(&snapshot2, &[HashAlgorithm::Sha256]) + .unwrap() + .version(2) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + let metadata2 = RawSignedMetadataSetBuilder::new() + .root(raw_root2) + .targets(raw_targets2) + .snapshot(raw_snapshot2) + .timestamp(raw_timestamp2) + .build(); + + assert_matches!( + tuf.update_metadata(&metadata2), + Err(Error::MetadataMissingSignatures { + role, + number_of_valid_signatures: 0, + threshold: 1, + }) + if role == MetadataPath::root() + ); + } +} diff --git a/vendor/tuf/src/error.rs b/vendor/tuf/src/error.rs new file mode 100644 index 0000000000..8165f2fffb --- /dev/null +++ b/vendor/tuf/src/error.rs @@ -0,0 +1,183 @@ +//! Error types and converters. + +use std::io; +use thiserror::Error; + +use crate::metadata::{MetadataPath, MetadataVersion, TargetPath}; + +/// Alias for `Result`. +pub type Result = std::result::Result; + +/// Error type for all TUF related errors. +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum Error { + /// The metadata had a bad signature. + #[error("metadata {0} has a bad signature")] + BadSignature(MetadataPath), + + /// There was a problem encoding or decoding. + #[error("encoding: {0}")] + Encoding(String), + + /// Metadata was expired. + #[error("expired {0} metadata")] + ExpiredMetadata(MetadataPath), + + /// An illegal argument was passed into a function. + #[error("illegal argument: {0}")] + IllegalArgument(String), + + /// Generic error for HTTP connections. + #[error("http error for {uri}")] + Http { + /// URI Resource that resulted in the error. + uri: String, + + /// The error. + #[source] + err: http::Error, + }, + + /// Errors that can occur parsing HTTP streams. + #[cfg(feature = "hyper")] + #[error("hyper error for {uri}")] + Hyper { + /// URI Resource that resulted in the error. + uri: String, + + /// The error. + #[source] + err: hyper::Error, + }, + + /// Unexpected HTTP response status. + #[error("error getting {uri}: request failed with status code {code}")] + BadHttpStatus { + /// URI Resource that resulted in the error. + uri: String, + + /// HTTP status code. + code: http::StatusCode, + }, + + /// An IO error occurred. + #[error(transparent)] + Io(#[from] io::Error), + + /// An IO error occurred for a path. + #[error("IO error on path {path}")] + IoPath { + /// Path where the error occurred. + path: std::path::PathBuf, + + /// The IO error. + #[source] + err: io::Error, + }, + + /// A json serialization error occurred. + #[error(transparent)] + Json(#[from] serde_json::error::Error), + + /// There were no available hash algorithms. + #[error("no supported hash algorithm")] + NoSupportedHashAlgorithm, + + /// The metadata was not found. + #[error("metadata {path} at version {version} not found")] + MetadataNotFound { + /// The metadata path. + path: MetadataPath, + + /// The metadata version. + version: MetadataVersion, + }, + + /// The target was not found. + #[error("target {0} not found")] + TargetNotFound(TargetPath), + + /// Opaque error type, to be interpreted similar to HTTP 500. Something went wrong, and you may + /// or may not be able to do anything about it. + #[error("opaque: {0}")] + Opaque(String), + + /// There is no known or available key type. + #[error("unknown key type: {0}")] + UnknownKeyType(String), + + /// There is no known or available signature scheme. + #[error("unknown signature scheme: {0}")] + UnknownSignatureScheme(String), + + /// The metadata threshold cannot equal 0. + #[error("metadata {0} threshold must be greater than zero")] + MetadataThresholdMustBeGreaterThanZero(MetadataPath), + + /// The metadata's version must be less than `u64::MAX`. + #[error("metadata {0} version should be less than max u64")] + MetadataVersionMustBeSmallerThanMaxU64(MetadataPath), + + /// The metadata was not signed with enough valid signatures. + #[error( + "metadata {role} signature threshold not met: {number_of_valid_signatures}/{threshold}" + )] + MetadataMissingSignatures { + /// The signed metadata. + role: MetadataPath, + /// The number of signatures which are valid. + number_of_valid_signatures: u32, + /// The minimum number of valid signatures. + threshold: u32, + }, + + /// Attempted to update metadata with an older version. + #[error( + "attempted to roll back metadata {role} from version {trusted_version} to {new_version}" + )] + AttemptedMetadataRollBack { + /// The metadata. + role: MetadataPath, + /// The trusted metadata's version. + trusted_version: u64, + /// The new metadata's version. + new_version: u64, + }, + + /// The parent metadata expected the child metadata to be at one version, but was found to be at + /// another version. + #[error("metadata {parent_role} expected metadata {child_role} version {expected_version}, but found {new_version}")] + WrongMetadataVersion { + /// The parent metadata that contains the child metadata's version. + parent_role: MetadataPath, + /// The child metadata that has an unexpected version. + child_role: MetadataPath, + /// The expected version of the child metadata. + expected_version: u64, + /// The actual version of the child metadata. + new_version: u64, + }, + + /// The parent metadata does not contain a description of the child metadata. + #[error("metadata {parent_role} missing description of {child_role}")] + MissingMetadataDescription { + /// The parent metadata that contains the child metadata's description. + parent_role: MetadataPath, + /// The child metadata that should have been contained in the parent. + child_role: MetadataPath, + }, + + /// The parent metadata did not delegate to the child role. + #[error("{parent_role} delegation to {child_role} is not authorized")] + UnauthorizedDelegation { + /// The parent metadata that did not delegate to the child. + parent_role: MetadataPath, + /// That child metadata that was not delegated to by the parent. + child_role: MetadataPath, + }, +} + +pub(crate) fn derp_error_to_error(err: derp::Error) -> Error { + Error::Encoding(format!("DER: {:?}", err)) +} diff --git a/vendor/tuf/src/format_hex.rs b/vendor/tuf/src/format_hex.rs new file mode 100644 index 0000000000..2ca85c4afc --- /dev/null +++ b/vendor/tuf/src/format_hex.rs @@ -0,0 +1,20 @@ +use data_encoding::HEXLOWER; +use serde::{self, Deserialize, Deserializer, Serializer}; +use std::result::Result; + +pub fn serialize(value: &[u8], serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&HEXLOWER.encode(value)) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + HEXLOWER + .decode(s.as_bytes()) + .map_err(serde::de::Error::custom) +} diff --git a/vendor/tuf/src/interchange/cjson/mod.rs b/vendor/tuf/src/interchange/cjson/mod.rs new file mode 100644 index 0000000000..ea64ab3f8d --- /dev/null +++ b/vendor/tuf/src/interchange/cjson/mod.rs @@ -0,0 +1,450 @@ +use serde::de::DeserializeOwned; +use serde::ser::Serialize; +use std::collections::BTreeMap; + +use crate::error::Error; +use crate::interchange::DataInterchange; +use crate::Result; + +pub(crate) mod pretty; +pub(crate) mod shims; + +pub use pretty::JsonPretty; + +/// JSON data interchange. +/// +/// # Schema +/// +/// This doesn't use JSON Schema because that specification language is rage inducing. Here's +/// something else instead. +/// +/// ## Common Entities +/// +/// `NATURAL_NUMBER` is an integer in the range `[1, 2**32)`. +/// +/// `EXPIRES` is an ISO-8601 date time in format `YYYY-MM-DD'T'hh:mm:ss'Z'`. +/// +/// `KEY_ID` is the hex encoded value of `sha256(cjson(pub_key))`. +/// +/// `PUB_KEY` is the following: +/// +/// ```bash +/// { +/// "type": KEY_TYPE, +/// "scheme": SCHEME, +/// "value": PUBLIC +/// } +/// ``` +/// +/// `PUBLIC` is a base64url encoded `SubjectPublicKeyInfo` DER public key. +/// +/// `KEY_TYPE` is a string (either `rsa` or `ed25519`). +/// +/// `SCHEME` is a string (either `ed25519`, `rsassa-pss-sha256`, or `rsassa-pss-sha512` +/// +/// `HASH_VALUE` is a hex encoded hash value. +/// +/// `SIG_VALUE` is a hex encoded signature value. +/// +/// `METADATA_DESCRIPTION` is the following: +/// +/// ```bash +/// { +/// "version": NATURAL_NUMBER, +/// "length": NATURAL_NUMBER, +/// "hashes": { +/// HASH_ALGORITHM: HASH_VALUE +/// ... +/// } +/// } +/// ``` +/// +/// ## `SignedMetadata` +/// +/// ```bash +/// { +/// "signatures": [SIGNATURE], +/// "signed": SIGNED +/// } +/// ``` +/// +/// `SIGNATURE` is: +/// +/// ```bash +/// { +/// "keyid": KEY_ID, +/// "signature": SIG_VALUE +/// } +/// ``` +/// +/// `SIGNED` is one of: +/// +/// - `RootMetadata` +/// - `SnapshotMetadata` +/// - `TargetsMetadata` +/// - `TimestampMetadata` +/// +/// The the elements of `signatures` must have unique `key_id`s. +/// +/// ## `RootMetadata` +/// +/// ```bash +/// { +/// "_type": "root", +/// "version": NATURAL_NUMBER, +/// "expires": EXPIRES, +/// "keys": [PUB_KEY, ...] +/// "roles": { +/// "root": ROLE_DESCRIPTION, +/// "snapshot": ROLE_DESCRIPTION, +/// "targets": ROLE_DESCRIPTION, +/// "timestamp": ROLE_DESCRIPTION +/// } +/// } +/// ``` +/// +/// `ROLE_DESCRIPTION` is the following: +/// +/// ```bash +/// { +/// "threshold": NATURAL_NUMBER, +/// "keyids": [KEY_ID, ...] +/// } +/// ``` +/// +/// ## `SnapshotMetadata` +/// +/// ```bash +/// { +/// "_type": "snapshot", +/// "version": NATURAL_NUMBER, +/// "expires": EXPIRES, +/// "meta": { +/// META_PATH: METADATA_DESCRIPTION +/// } +/// } +/// ``` +/// +/// `META_PATH` is a string. +/// +/// +/// ## `TargetsMetadata` +/// +/// ```bash +/// { +/// "_type": "timestamp", +/// "version": NATURAL_NUMBER, +/// "expires": EXPIRES, +/// "targets": { +/// TARGET_PATH: TARGET_DESCRIPTION +/// ... +/// }, +/// "delegations": DELEGATIONS +/// } +/// ``` +/// +/// `DELEGATIONS` is optional and is described by the following: +/// +/// ```bash +/// { +/// "keys": [PUB_KEY, ...] +/// "roles": { +/// ROLE: DELEGATION, +/// ... +/// } +/// } +/// ``` +/// +/// `DELEGATION` is: +/// +/// ```bash +/// { +/// "name": ROLE, +/// "threshold": NATURAL_NUMBER, +/// "terminating": BOOLEAN, +/// "keyids": [KEY_ID, ...], +/// "paths": [PATH, ...] +/// } +/// ``` +/// +/// `ROLE` is a string, +/// +/// `PATH` is a string. +/// +/// ## `TimestampMetadata` +/// +/// ```bash +/// { +/// "_type": "timestamp", +/// "version": NATURAL_NUMBER, +/// "expires": EXPIRES, +/// "snapshot": METADATA_DESCRIPTION +/// } +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Json; + +impl DataInterchange for Json { + type RawData = serde_json::Value; + + /// ``` + /// # use tuf::interchange::{DataInterchange, Json}; + /// assert_eq!(Json::extension(), "json"); + /// ``` + fn extension() -> &'static str { + "json" + } + + /// ``` + /// # use tuf::interchange::{DataInterchange, Json}; + /// # use std::collections::HashMap; + /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; + /// let raw = Json::from_slice(jsn).unwrap(); + /// let out = Json::canonicalize(&raw).unwrap(); + /// assert_eq!(out, br#"{"baz":"quux","foo":"bar"}"#); + /// ``` + fn canonicalize(raw_data: &Self::RawData) -> Result> { + canonicalize(raw_data).map_err(Error::Opaque) + } + + /// ``` + /// # use serde_derive::Deserialize; + /// # use serde_json::json; + /// # use std::collections::HashMap; + /// # use tuf::interchange::{DataInterchange, Json}; + /// # + /// #[derive(Deserialize, Debug, PartialEq)] + /// struct Thing { + /// foo: String, + /// bar: String, + /// } + /// + /// let jsn = json!({"foo": "wat", "bar": "lol"}); + /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; + /// let de: Thing = Json::deserialize(&jsn).unwrap(); + /// assert_eq!(de, thing); + /// ``` + fn deserialize(raw_data: &Self::RawData) -> Result + where + T: DeserializeOwned, + { + Ok(serde_json::from_value(raw_data.clone())?) + } + + /// ``` + /// # use serde_derive::Serialize; + /// # use serde_json::json; + /// # use std::collections::HashMap; + /// # use tuf::interchange::{DataInterchange, Json}; + /// # + /// #[derive(Serialize)] + /// struct Thing { + /// foo: String, + /// bar: String, + /// } + /// + /// let jsn = json!({"foo": "wat", "bar": "lol"}); + /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; + /// let se: serde_json::Value = Json::serialize(&thing).unwrap(); + /// assert_eq!(se, jsn); + /// ``` + fn serialize(data: &T) -> Result + where + T: Serialize, + { + Ok(serde_json::to_value(data)?) + } + + /// ``` + /// # use tuf::interchange::{DataInterchange, Json}; + /// # use std::collections::HashMap; + /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; + /// let _: HashMap = Json::from_slice(&jsn).unwrap(); + /// ``` + fn from_slice(slice: &[u8]) -> Result + where + T: DeserializeOwned, + { + Ok(serde_json::from_slice(slice)?) + } +} + +fn canonicalize(jsn: &serde_json::Value) -> std::result::Result, String> { + let converted = convert(jsn)?; + let mut buf = Vec::new(); + let _ = converted.write(&mut buf); // Vec impl always succeeds (or panics). + Ok(buf) +} + +enum Value { + Array(Vec), + Bool(bool), + Null, + Number(Number), + Object(BTreeMap), + String(String), +} + +impl Value { + fn write(&self, buf: &mut Vec) -> std::result::Result<(), String> { + match *self { + Value::Null => { + buf.extend(b"null"); + Ok(()) + } + Value::Bool(true) => { + buf.extend(b"true"); + Ok(()) + } + Value::Bool(false) => { + buf.extend(b"false"); + Ok(()) + } + Value::Number(Number::I64(n)) => itoa::write(buf, n) + .map(|_| ()) + .map_err(|err| format!("Write error: {}", err)), + Value::Number(Number::U64(n)) => itoa::write(buf, n) + .map(|_| ()) + .map_err(|err| format!("Write error: {}", err)), + Value::String(ref s) => { + // OLPC Canonical JSON (https://wiki.laptop.org/go/Canonical_JSON): escape only + // `\` and `"`; all other bytes — including control chars — emit literally. + buf.push(b'"'); + for &byte in s.as_bytes() { + match byte { + b'\\' => buf.extend_from_slice(b"\\\\"), + b'"' => buf.extend_from_slice(b"\\\""), + other => buf.push(other), + } + } + buf.push(b'"'); + Ok(()) + } + Value::Array(ref arr) => { + buf.push(b'['); + let mut first = true; + for a in arr.iter() { + if !first { + buf.push(b','); + } + a.write(buf)?; + first = false; + } + buf.push(b']'); + Ok(()) + } + Value::Object(ref obj) => { + buf.push(b'{'); + let mut first = true; + for (k, v) in obj.iter() { + if !first { + buf.push(b','); + } + first = false; + + // this mess is abusing serde_json to get json escaping + let k = serde_json::Value::String(k.clone()); + let k = serde_json::to_string(&k).map_err(|e| format!("{:?}", e))?; + buf.extend(k.as_bytes()); + + buf.push(b':'); + v.write(buf)?; + } + buf.push(b'}'); + Ok(()) + } + } + } +} + +enum Number { + I64(i64), + U64(u64), +} + +fn convert(jsn: &serde_json::Value) -> std::result::Result { + match *jsn { + serde_json::Value::Null => Ok(Value::Null), + serde_json::Value::Bool(b) => Ok(Value::Bool(b)), + serde_json::Value::Number(ref n) => n + .as_i64() + .map(Number::I64) + .or_else(|| n.as_u64().map(Number::U64)) + .map(Value::Number) + .ok_or_else(|| String::from("only i64 and u64 are supported")), + serde_json::Value::Array(ref arr) => { + let mut out = Vec::new(); + for res in arr.iter().map(convert) { + out.push(res?) + } + Ok(Value::Array(out)) + } + serde_json::Value::Object(ref obj) => { + let mut out = BTreeMap::new(); + for (k, v) in obj.iter() { + let _ = out.insert(k.clone(), convert(v)?); + } + Ok(Value::Object(out)) + } + serde_json::Value::String(ref s) => Ok(Value::String(s.clone())), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn write_str() { + let jsn = Value::String(String::from("wat")); + let mut out = Vec::new(); + jsn.write(&mut out).unwrap(); + assert_eq!(&out, b"\"wat\""); + } + + #[test] + fn write_arr() { + let jsn = Value::Array(vec![ + Value::String(String::from("wat")), + Value::String(String::from("lol")), + Value::String(String::from("no")), + ]); + let mut out = Vec::new(); + jsn.write(&mut out).unwrap(); + assert_eq!(&out, b"[\"wat\",\"lol\",\"no\"]"); + } + + #[test] + fn write_obj() { + let mut map = BTreeMap::new(); + let arr = Value::Array(vec![ + Value::String(String::from("haha")), + // OLPC canonical JSON keeps control characters literal — the LF byte stays as 0x0a. + Value::String(String::from("new\nline")), + ]); + let _ = map.insert(String::from("lol"), arr); + let jsn = Value::Object(map); + let mut out = Vec::new(); + jsn.write(&mut out).unwrap(); + assert_eq!(&out, &b"{\"lol\":[\"haha\",\"new\nline\"]}"); + } + + #[test] + fn write_string_olpc_only_escapes_quote_and_backslash() { + for (input, expected) in [ + // Backslash and double-quote get escaped. + ("\\", b"\"\\\\\"" as &[u8]), + ("\"", b"\"\\\"\""), + ("a\\b\"c", b"\"a\\\\b\\\"c\""), + // Other control characters are NOT escaped. + ("a\nb", b"\"a\nb\""), + ("\t\r\x08\x0c", b"\"\t\r\x08\x0c\""), + // Non-ASCII UTF-8 is emitted literally. + ("résumé", "\"résumé\"".as_bytes()), + ] { + let mut out = Vec::new(); + Value::String(input.to_string()).write(&mut out).unwrap(); + assert_eq!(&out, expected, "input was {:?}", input); + } + } +} diff --git a/vendor/tuf/src/interchange/cjson/pretty.rs b/vendor/tuf/src/interchange/cjson/pretty.rs new file mode 100644 index 0000000000..ffa8d121b7 --- /dev/null +++ b/vendor/tuf/src/interchange/cjson/pretty.rs @@ -0,0 +1,199 @@ +use serde::de::DeserializeOwned; +use serde::ser::Serialize; +use serde_json::{Map, Value}; + +use super::Json; +use crate::interchange::DataInterchange; +use crate::Result; + +/// Pretty JSON data interchange. +/// +/// This is identical to [Json] in all manners except for the `canonicalize` method. Instead of +/// writing the metadata in the canonical format, it first canonicalizes it, then pretty prints +/// the metadata. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JsonPretty; + +impl DataInterchange for JsonPretty { + type RawData = serde_json::Value; + + /// ``` + /// # use tuf::interchange::{DataInterchange, JsonPretty}; + /// assert_eq!(JsonPretty::extension(), "json"); + /// ``` + fn extension() -> &'static str { + Json::extension() + } + + /// ``` + /// # use serde_json::json; + /// # use tuf::interchange::{DataInterchange, JsonPretty}; + /// # + /// let json = json!({ + /// "o": { + /// "a": [1, 2, 3], + /// "s": "string", + /// "n": 123, + /// "t": true, + /// "f": false, + /// "0": null, + /// }, + /// }); + /// + /// let bytes = JsonPretty::canonicalize(&json).unwrap(); + /// + /// assert_eq!(&String::from_utf8(bytes).unwrap(), r#"{ + /// "o": { + /// "0": null, + /// "a": [ + /// 1, + /// 2, + /// 3 + /// ], + /// "f": false, + /// "n": 123, + /// "s": "string", + /// "t": true + /// } + /// }"#); + /// ``` + fn canonicalize(raw_data: &Self::RawData) -> Result> { + // Sort explicitly: `Value::Object` is `IndexMap` (insertion order) when any workspace + // crate enables `serde_json/preserve_order`, so we can't rely on the `Map` type alone. + Ok(serde_json::to_vec_pretty(&with_sorted_keys(raw_data))?) + } + + /// ``` + /// # use serde_derive::Deserialize; + /// # use serde_json::json; + /// # use std::collections::HashMap; + /// # use tuf::interchange::{DataInterchange, JsonPretty}; + /// # + /// #[derive(Deserialize, Debug, PartialEq)] + /// struct Thing { + /// foo: String, + /// bar: String, + /// } + /// + /// let jsn = json!({"foo": "wat", "bar": "lol"}); + /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; + /// let de: Thing = JsonPretty::deserialize(&jsn).unwrap(); + /// assert_eq!(de, thing); + /// ``` + fn deserialize(raw_data: &Self::RawData) -> Result + where + T: DeserializeOwned, + { + Json::deserialize(raw_data) + } + + /// ``` + /// # use serde_derive::Serialize; + /// # use serde_json::json; + /// # use std::collections::HashMap; + /// # use tuf::interchange::{DataInterchange, JsonPretty}; + /// # + /// #[derive(Serialize)] + /// struct Thing { + /// foo: String, + /// bar: String, + /// } + /// + /// let jsn = json!({"foo": "wat", "bar": "lol"}); + /// let thing = Thing { foo: "wat".into(), bar: "lol".into() }; + /// let se: serde_json::Value = JsonPretty::serialize(&thing).unwrap(); + /// assert_eq!(se, jsn); + /// ``` + fn serialize(data: &T) -> Result + where + T: Serialize, + { + Json::serialize(data) + } + + /// ``` + /// # use tuf::interchange::{DataInterchange, JsonPretty}; + /// # use std::collections::HashMap; + /// let jsn: &[u8] = br#"{"foo": "bar", "baz": "quux"}"#; + /// let _: HashMap = JsonPretty::from_slice(&jsn).unwrap(); + /// ``` + fn from_slice(slice: &[u8]) -> Result + where + T: DeserializeOwned, + { + Json::from_slice(slice) + } +} + +/// Rebuild a `Value` with every object's keys inserted in sorted order, so re-serialization +/// emits them sorted regardless of whether `serde_json::Map` is `BTreeMap` or `IndexMap`. +fn with_sorted_keys(value: &Value) -> Value { + match value { + Value::Object(map) => { + let mut entries: Vec<(&String, &Value)> = map.iter().collect(); + entries.sort_by(|a, b| a.0.cmp(b.0)); + let mut sorted = Map::with_capacity(entries.len()); + for (k, v) in entries { + sorted.insert(k.clone(), with_sorted_keys(v)); + } + Value::Object(sorted) + } + Value::Array(arr) => Value::Array(arr.iter().map(with_sorted_keys).collect()), + other => other.clone(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + /// Strings with control characters (literal newlines from PEM keyvals, tabs, etc.) must + /// canonicalize without error. + #[test] + fn canonicalize_handles_strings_with_control_characters() { + let value = json!({ + "key_with_newlines": "-----BEGIN PUBLIC KEY-----\nABC\n-----END PUBLIC KEY-----\n", + "key_with_tab": "a\tb", + }); + let bytes = JsonPretty::canonicalize(&value).expect("must not fail on control chars"); + + // Round-trips back to the same value. + let reparsed: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(reparsed, value); + + // Pretty output sorts top-level keys (alphabetical: `key_with_newlines`, `key_with_tab`). + let s = std::str::from_utf8(&bytes).unwrap(); + assert!(s.find("key_with_newlines").unwrap() < s.find("key_with_tab").unwrap()); + } + + /// Object keys must come out sorted even when the underlying `Map` preserves insertion + /// order (`tuf/Cargo.toml` enables `serde_json/preserve_order` as a dev-dep so this runs + /// against `IndexMap`). Inserting in reverse-alphabetical order would otherwise yield + /// reverse-alphabetical output if the canonicalizer didn't explicitly sort. + #[test] + fn canonicalize_sorts_keys_recursively_against_insertion_order() { + let mut top = serde_json::Map::new(); + // Insert in reverse-alphabetical order; under preserve_order this is the iteration + // order, under BTreeMap it gets sorted. + let mut nested = serde_json::Map::new(); + nested.insert("z_inner".to_string(), json!(1)); + nested.insert("a_inner".to_string(), json!(2)); + top.insert("z_top".to_string(), serde_json::Value::Object(nested)); + top.insert("a_top".to_string(), json!("first alphabetically")); + + let value = serde_json::Value::Object(top); + let bytes = JsonPretty::canonicalize(&value).unwrap(); + let pretty = std::str::from_utf8(&bytes).unwrap(); + + // Top-level: a_top before z_top. + let a_top = pretty.find("a_top").unwrap(); + let z_top = pretty.find("z_top").unwrap(); + assert!(a_top < z_top, "top-level keys must sort: {pretty}"); + + // Nested object: a_inner before z_inner. + let a_inner = pretty.find("a_inner").unwrap(); + let z_inner = pretty.find("z_inner").unwrap(); + assert!(a_inner < z_inner, "nested keys must sort: {pretty}"); + } +} diff --git a/vendor/tuf/src/interchange/cjson/shims.rs b/vendor/tuf/src/interchange/cjson/shims.rs new file mode 100644 index 0000000000..a29f37153a --- /dev/null +++ b/vendor/tuf/src/interchange/cjson/shims.rs @@ -0,0 +1,649 @@ +use chrono::offset::Utc; +use chrono::prelude::*; +use serde_derive::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashSet}; + +use crate::crypto; +use crate::error::Error; +use crate::metadata::{self, Metadata}; +use crate::Result; + +const SPEC_VERSION: &str = "1.0.0"; + +// Ensure the given spec version matches our spec version. +// +// We also need to handle the literal "1.0" here, despite that fact that it is not a valid version +// according to the SemVer spec, because it is already baked into some of the old roots. +fn valid_spec_version(other: &str) -> bool { + other == SPEC_VERSION || other == "1.0" +} + +fn parse_datetime(ts: &str) -> Result> { + DateTime::parse_from_rfc3339(ts) + .map(|ts| ts.with_timezone(&Utc)) + .map_err(|e| Error::Encoding(format!("Can't parse DateTime: {:?}", e))) +} + +fn format_datetime(ts: &DateTime) -> String { + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + ts.year(), + ts.month(), + ts.day(), + ts.hour(), + ts.minute(), + ts.second() + ) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RootMetadata { + #[serde(rename = "_type")] + typ: metadata::Role, + spec_version: String, + version: u64, + consistent_snapshot: bool, + expires: String, + #[serde(deserialize_with = "deserialize_reject_duplicates::deserialize")] + keys: BTreeMap, + roles: RoleDefinitions, + #[serde(flatten)] + additional_fields: BTreeMap, +} + +impl RootMetadata { + pub fn from(meta: &metadata::RootMetadata) -> Result { + Ok(RootMetadata { + typ: metadata::Role::Root, + spec_version: SPEC_VERSION.to_string(), + version: meta.version(), + expires: format_datetime(meta.expires()), + consistent_snapshot: meta.consistent_snapshot(), + keys: meta + .keys() + .iter() + .map(|(id, key)| (id.clone(), key.clone())) + .collect(), + roles: RoleDefinitions { + root: meta.root().clone(), + snapshot: meta.snapshot().clone(), + targets: meta.targets().clone(), + timestamp: meta.timestamp().clone(), + }, + additional_fields: meta.additional_fields().clone().into_iter().collect(), + }) + } + + pub fn try_into(self) -> Result { + if self.typ != metadata::Role::Root { + return Err(Error::Encoding(format!( + "Attempted to decode root metdata labeled as {:?}", + self.typ + ))); + } + + if !valid_spec_version(&self.spec_version) { + return Err(Error::Encoding(format!( + "Unknown spec version {}", + self.spec_version + ))); + } + + // Ignore all keys with incorrect key IDs. We should give an error if the key ID is not + // correct according to TUF spec. However, due to backward compatibility, we may receive + // metadata with key IDs generated by TUF 0.9. We simply ignore those old keys. + let keys_with_correct_key_id = self + .keys + .into_iter() + .filter(|(key_id, pkey)| key_id == pkey.key_id()) + .collect(); + + metadata::RootMetadata::new( + self.version, + parse_datetime(&self.expires)?, + self.consistent_snapshot, + keys_with_correct_key_id, + self.roles.root, + self.roles.snapshot, + self.roles.targets, + self.roles.timestamp, + self.additional_fields.into_iter().collect(), + ) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct RoleDefinitions { + root: metadata::RoleDefinition, + snapshot: metadata::RoleDefinition, + targets: metadata::RoleDefinition, + timestamp: metadata::RoleDefinition, +} + +#[derive(Serialize, Deserialize)] +pub struct RoleDefinition { + threshold: u32, + #[serde(rename = "keyids")] + key_ids: Vec, +} + +impl RoleDefinition { + pub fn from(role: &metadata::RoleDefinition) -> Result { + let mut key_ids = role.key_ids().iter().cloned().collect::>(); + key_ids.sort(); + + Ok(RoleDefinition { + threshold: role.threshold(), + key_ids, + }) + } + + pub fn try_into(self) -> Result { + let key_ids_len = self.key_ids.len(); + if key_ids_len < 1 { + return Err(Error::Encoding( + "Role defined with no assoiciated key IDs.".into(), + )); + } + + let key_ids = self.key_ids.into_iter().collect::>(); + + if key_ids.len() != key_ids_len { + return Err(Error::Encoding(format!( + "Found {} duplicate key IDs.", + key_ids_len - key_ids.len() + ))); + } + + metadata::RoleDefinition::new(self.threshold, key_ids) + } +} + +#[derive(Serialize, Deserialize)] +pub struct TimestampMetadata { + #[serde(rename = "_type")] + typ: metadata::Role, + spec_version: String, + version: u64, + expires: String, + meta: TimestampMeta, + #[serde(flatten)] + additional_fields: BTreeMap, +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct TimestampMeta { + #[serde(rename = "snapshot.json")] + snapshot: metadata::MetadataDescription, +} + +impl TimestampMetadata { + pub fn from(metadata: &metadata::TimestampMetadata) -> Result { + Ok(TimestampMetadata { + typ: metadata::Role::Timestamp, + spec_version: SPEC_VERSION.to_string(), + version: metadata.version(), + expires: format_datetime(metadata.expires()), + meta: TimestampMeta { + snapshot: metadata.snapshot().clone(), + }, + additional_fields: metadata.additional_fields().clone().into_iter().collect(), + }) + } + + pub fn try_into(self) -> Result { + if self.typ != metadata::Role::Timestamp { + return Err(Error::Encoding(format!( + "Attempted to decode timestamp metdata labeled as {:?}", + self.typ + ))); + } + + if !valid_spec_version(&self.spec_version) { + return Err(Error::Encoding(format!( + "Unknown spec version {}", + self.spec_version + ))); + } + + metadata::TimestampMetadata::new( + self.version, + parse_datetime(&self.expires)?, + self.meta.snapshot, + self.additional_fields.into_iter().collect(), + ) + } +} + +#[derive(Serialize, Deserialize)] +pub struct SnapshotMetadata { + #[serde(rename = "_type")] + typ: metadata::Role, + spec_version: String, + version: u64, + expires: String, + #[serde(deserialize_with = "deserialize_reject_duplicates::deserialize")] + meta: BTreeMap, + #[serde(flatten)] + additional_fields: BTreeMap, +} + +impl SnapshotMetadata { + pub fn from(metadata: &metadata::SnapshotMetadata) -> Result { + Ok(SnapshotMetadata { + typ: metadata::Role::Snapshot, + spec_version: SPEC_VERSION.to_string(), + version: metadata.version(), + expires: format_datetime(metadata.expires()), + meta: metadata + .meta() + .iter() + .map(|(p, d)| (format!("{}.json", p), d.clone())) + .collect(), + additional_fields: metadata.additional_fields().clone().into_iter().collect(), + }) + } + + pub fn try_into(self) -> Result { + if self.typ != metadata::Role::Snapshot { + return Err(Error::Encoding(format!( + "Attempted to decode snapshot metdata labeled as {:?}", + self.typ + ))); + } + + if !valid_spec_version(&self.spec_version) { + return Err(Error::Encoding(format!( + "Unknown spec version {}", + self.spec_version + ))); + } + + metadata::SnapshotMetadata::new( + self.version, + parse_datetime(&self.expires)?, + self.meta + .into_iter() + .map(|(p, d)| { + if !p.ends_with(".json") { + return Err(Error::Encoding(format!( + "Metadata does not end with .json: {}", + p + ))); + } + + let s = p.split_at(p.len() - ".json".len()).0.to_owned(); + let p = metadata::MetadataPath::new(s)?; + + Ok((p, d)) + }) + .collect::>()?, + self.additional_fields.into_iter().collect(), + ) + } +} + +#[derive(Serialize, Deserialize)] +pub struct TargetsMetadata { + #[serde(rename = "_type")] + typ: metadata::Role, + spec_version: String, + version: u64, + expires: String, + targets: BTreeMap, + #[serde(default, skip_serializing_if = "metadata::Delegations::is_empty")] + delegations: metadata::Delegations, + #[serde(flatten)] + additional_fields: BTreeMap, +} + +impl TargetsMetadata { + pub fn from(metadata: &metadata::TargetsMetadata) -> Result { + Ok(TargetsMetadata { + typ: metadata::Role::Targets, + spec_version: SPEC_VERSION.to_string(), + version: metadata.version(), + expires: format_datetime(metadata.expires()), + targets: metadata + .targets() + .iter() + .map(|(p, d)| (p.clone(), d.clone())) + .collect(), + delegations: metadata.delegations().clone(), + additional_fields: metadata + .additional_fields() + .iter() + .map(|(p, d)| (p.clone(), d.clone())) + .collect(), + }) + } + + pub fn try_into(self) -> Result { + if self.typ != metadata::Role::Targets { + return Err(Error::Encoding(format!( + "Attempted to decode targets metdata labeled as {:?}", + self.typ + ))); + } + + if !valid_spec_version(&self.spec_version) { + return Err(Error::Encoding(format!( + "Unknown spec version {}", + self.spec_version + ))); + } + + metadata::TargetsMetadata::new( + self.version, + parse_datetime(&self.expires)?, + self.targets.into_iter().collect(), + self.delegations, + self.additional_fields.into_iter().collect(), + ) + } +} + +#[derive(Serialize, Deserialize)] +pub struct PublicKey { + keytype: crypto::KeyType, + scheme: crypto::SignatureScheme, + #[serde(skip_serializing_if = "Option::is_none")] + keyid_hash_algorithms: Option>, + keyval: PublicKeyValue, +} + +impl PublicKey { + pub fn new( + keytype: crypto::KeyType, + scheme: crypto::SignatureScheme, + keyid_hash_algorithms: Option>, + public_key: String, + ) -> Self { + PublicKey { + keytype, + scheme, + keyid_hash_algorithms, + keyval: PublicKeyValue { public: public_key }, + } + } + + pub fn public_key(&self) -> &str { + &self.keyval.public + } + + pub fn scheme(&self) -> &crypto::SignatureScheme { + &self.scheme + } + + pub fn keytype(&self) -> &crypto::KeyType { + &self.keytype + } + + pub fn keyid_hash_algorithms(&self) -> &Option> { + &self.keyid_hash_algorithms + } +} + +#[derive(Serialize, Deserialize)] +pub struct PublicKeyValue { + public: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Delegation { + name: metadata::MetadataPath, + terminating: bool, + threshold: u32, + #[serde(rename = "keyids")] + key_ids: Vec, + paths: Vec, +} + +impl From<&metadata::Delegation> for Delegation { + fn from(delegation: &metadata::Delegation) -> Self { + let mut paths = delegation + .paths() + .iter() + .cloned() + .collect::>(); + paths.sort(); + + let mut key_ids = delegation + .key_ids() + .iter() + .cloned() + .collect::>(); + key_ids.sort(); + + Delegation { + name: delegation.name().clone(), + terminating: delegation.terminating(), + threshold: delegation.threshold(), + key_ids, + paths, + } + } +} + +impl TryFrom for metadata::Delegation { + type Error = Error; + + fn try_from(delegation: Delegation) -> Result { + let delegation_key_ids_len = delegation.key_ids.len(); + let key_ids = delegation.key_ids.into_iter().collect::>(); + + if key_ids.len() != delegation_key_ids_len { + return Err(Error::Encoding("Non-unique delegation key IDs.".into())); + } + + let delegation_paths_len = delegation.paths.len(); + let paths = delegation.paths.into_iter().collect::>(); + + if paths.len() != delegation_paths_len { + return Err(Error::Encoding("Non-unique delegation paths.".into())); + } + + metadata::Delegation::new( + delegation.name, + delegation.terminating, + delegation.threshold, + key_ids, + paths, + ) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Delegations { + #[serde(deserialize_with = "deserialize_reject_duplicates::deserialize")] + keys: BTreeMap, + roles: Vec, +} + +impl From<&metadata::Delegations> for Delegations { + fn from(delegations: &metadata::Delegations) -> Delegations { + let mut roles = delegations + .roles() + .iter() + .map(Delegation::from) + .collect::>(); + + // We want our roles in a consistent order. + roles.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + + Delegations { + keys: delegations + .keys() + .iter() + .map(|(id, key)| (id.clone(), key.clone())) + .collect(), + roles, + } + } +} + +impl TryFrom for metadata::Delegations { + type Error = Error; + + fn try_from(delegations: Delegations) -> Result { + metadata::Delegations::new( + delegations.keys.into_iter().collect(), + delegations + .roles + .into_iter() + .map(|delegation| delegation.try_into()) + .collect::>>()?, + ) + } +} + +#[derive(Serialize, Deserialize)] +pub struct TargetDescription { + length: u64, + hashes: BTreeMap, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + custom: BTreeMap, +} + +impl TargetDescription { + pub fn from(description: &metadata::TargetDescription) -> TargetDescription { + TargetDescription { + length: description.length(), + hashes: description + .hashes() + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + custom: description + .custom() + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + } + } + + pub fn try_into(self) -> Result { + metadata::TargetDescription::new( + self.length, + self.hashes.into_iter().collect(), + self.custom.into_iter().collect(), + ) + } +} + +#[derive(Deserialize)] +pub struct MetadataDescription { + version: u64, + #[serde(default)] + length: Option, + #[serde(default)] + hashes: BTreeMap, +} + +impl MetadataDescription { + pub fn try_into(self) -> Result { + metadata::MetadataDescription::new( + self.version, + self.length, + self.hashes.into_iter().collect(), + ) + } +} + +/// Custom deserialize to reject duplicate keys. +mod deserialize_reject_duplicates { + use serde::de::{Deserialize, Deserializer, Error, MapAccess, Visitor}; + use std::collections::BTreeMap; + use std::fmt; + use std::marker::PhantomData; + use std::result::Result; + + pub fn deserialize<'de, K, V, D>(deserializer: D) -> Result, D::Error> + where + K: Deserialize<'de> + Ord, + V: Deserialize<'de>, + D: Deserializer<'de>, + { + struct BTreeVisitor { + marker: PhantomData<(K, V)>, + } + + impl<'de, K, V> Visitor<'de> for BTreeVisitor + where + K: Deserialize<'de> + Ord, + V: Deserialize<'de>, + { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("map") + } + + fn visit_map(self, mut access: M) -> std::result::Result + where + M: MapAccess<'de>, + { + let mut map = BTreeMap::new(); + while let Some((key, value)) = access.next_entry()? { + if map.insert(key, value).is_some() { + return Err(M::Error::custom("Cannot have duplicate keys")); + } + } + Ok(map) + } + } + + deserializer.deserialize_map(BTreeVisitor { + marker: PhantomData, + }) + } +} + +#[cfg(test)] +mod test { + use super::{parse_datetime, valid_spec_version}; + + #[test] + fn spec_version_validation() { + let valid_spec_versions = ["1.0.0", "1.0"]; + + for version in valid_spec_versions { + assert!(valid_spec_version(version), "{:?} should be valid", version); + } + + let invalid_spec_versions = ["1.0.1", "1.1.0", "2.0.0", "3.0"]; + + for version in invalid_spec_versions { + assert!( + !valid_spec_version(version), + "{:?} should be invalid", + version + ); + } + } + + #[test] + fn datetime_formats() { + // The TUF spec says datetimes should be in ISO8601 format, specifically + // "YYYY-MM-DDTHH:MM:SSZ". Since not all TUF clients adhere strictly to that, we choose to + // be more lenient here. The following represent the intersection of valid ISO8601 + // and RFC3339 datetime formats (source: https://ijmacd.github.io/rfc3339-iso8601/). + let valid_formats = [ + "2022-08-30T19:53:55Z", + "2022-08-30T19:53:55.7Z", + "2022-08-30T19:53:55.77Z", + "2022-08-30T19:53:55.775Z", + "2022-08-30T19:53:55+00:00", + "2022-08-30T19:53:55.7+00:00", + "2022-08-30T14:53:55-05:00", + "2022-08-30T14:53:55.7-05:00", + "2022-08-30T14:53:55.77-05:00", + "2022-08-30T14:53:55.775-05:00", + ]; + + for format in valid_formats { + assert!(parse_datetime(format).is_ok(), "should parse {:?}", format); + } + } +} diff --git a/vendor/tuf/src/interchange/mod.rs b/vendor/tuf/src/interchange/mod.rs new file mode 100644 index 0000000000..f69b0e5aa2 --- /dev/null +++ b/vendor/tuf/src/interchange/mod.rs @@ -0,0 +1,37 @@ +//! Structures and functions to aid in various TUF data interchange formats. + +pub(crate) mod cjson; +pub use cjson::{Json, JsonPretty}; + +use serde::de::DeserializeOwned; +use serde::ser::Serialize; +use std::fmt::Debug; + +use crate::Result; + +/// The format used for data interchange, serialization, and deserialization. +pub trait DataInterchange: Debug + PartialEq + Clone + Send { + /// The type of data that is contained in the `signed` portion of metadata. + type RawData: Serialize + DeserializeOwned + Clone + PartialEq; + + /// The data interchange's extension. + fn extension() -> &'static str; + + /// A function that canonicalizes data to allow for deterministic signatures. + fn canonicalize(raw_data: &Self::RawData) -> Result>; + + /// Deserialize from `RawData`. + fn deserialize(raw_data: &Self::RawData) -> Result + where + T: DeserializeOwned; + + /// Serialize into `RawData`. + fn serialize(data: &T) -> Result + where + T: Serialize; + + /// Read a struct from a stream. + fn from_slice(slice: &[u8]) -> Result + where + T: DeserializeOwned; +} diff --git a/vendor/tuf/src/lib.rs b/vendor/tuf/src/lib.rs new file mode 100644 index 0000000000..89624f151e --- /dev/null +++ b/vendor/tuf/src/lib.rs @@ -0,0 +1,128 @@ +//! This crate provides an API for talking to repositories that implement The Update Framework +//! (TUF). +//! +//! If you are unfamiliar with TUF, you should read up on it via the [official +//! website](http://theupdateframework.github.io/). This crate aims to implement the entirety of +//! the specification as defined at the [head of the `develop` +//! branch](https://github.com/theupdateframework/tuf/blob/develop/docs/tuf-spec.txt) in the +//! official TUF git repository. +//! +//! Additionally, the following two papers are valuable supplements in understanding how to +//! actually implement TUF for a community repository. +//! +//! - [The Diplomat paper +//! (2016)](https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/kuppusamy) +//! - [The Mercury paper +//! (2017)](https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy) +//! +//! Failure to read the spec and the above papers will likely lead to an implementation that does +//! not take advantage of all the security guarantees that TUF offers. +//! +//! # Interoperability +//! +//! It should be noted that historically the TUF spec defined exactly one metadata format and one +//! way of organizing metadata within a repository. Thus, all TUF implementation could perfectly +//! interoperate. The TUF spec has moved to describing *how a framework should behave* leaving many +//! of the detais up to the implementor. Therefore, there are **zero** guarantees that this library +//! will work with any other TUF implementation. Should you want to access a TUF repository that +//! uses `rust-tuf` as its backend from another language, ASN.1 modules and metadata schemas are +//! provided that will allow you to interoperate with this library. +//! +//! # Implementation Considerations +//! +//! ## Key Management +//! +//! Part of TUF is that it acts as its own PKI, and there is no integration that needs to be done +//! for managing keys. +//! +//! Note: No two private keys that are generated should ever exist on the same hardware. When a +//! step says "generate `N` keys," the implication is that these `N` keys are generated on `N` +//! devices. +//! +//! The first set of keys that need to be generated at the root keys that are used to sign the root +//! metadata. The root should be defined with the following properties: +//! +//! - Minimum: +//! - 3 keys +//! - threshold of 2 +//! - Recommended: +//! - 5 keys +//! - threshold of 3 +//! +//! If a threshold of root keys are compromised, then the entire system is compromised and TUF +//! clients will need to be manually updated. Similarly, if some `X` keys are lost such that the +//! threshold `N` cannot be reached, then clients will also need to be manually updated. Both of +//! situations are considered critically unsafe. Whatever number of keys are used, it should be +//! assumed that some small number may be lost or compromised. +//! +//! These root keys **MUST** be kept offline on secure media. +//! +//! ## Delegations +//! +//! TUF's most useful feature is the ability to delegate certain roles to sign certain targets. +//! This is discussed in extensive detail in the aforementioned Diplomat paper. There are three +//! problems faced when delegating trust in TUF: +//! +//! 1. What to do for existent accounts that have not yet created and signed TUF metadata +//! 2. What to do when a new account registers +//! 3. What to do when an account uploads a new target and new metadata +//! +//! There are several approaches for dealing with the above scenarios. We are only going to discuss +//! on here as it is the recommended approach. This approach is taken directly from Section 6.1 of +//! the Diplomat paper +//! +//! ### Maximum Security Model +//! +//! The top-level targets role delegates to three other roles and are listed in the following order: +//! +//! 1. `claimed-projects` +//! - Terminating +//! - Delegates to project-specific roles that have registered keys with TUF +//! 2. `rarely-updated-projects` +//! - Terminating +//! - Signs all packages for all projects that have been "abandoned" or left unupdated for a long +//! time AND have not yet registered keys with TUF +//! 3. `new-projects` +//! - Non-terminating +//! - Signs all packages for all new projects as well as projects that were relegated to +//! `rarely-updated-projects` +//! +//! The top-level `targets` role as well as `claimed-projects` and `rarely-updated-projects` +//! **MUST** all use offline keys. +//! +//! The critical, manual step is to register new projects with TUF keys and move them into the +//! `claimed-projects` role. Projects that refuse to register keys should have their packages +//! periodically moved into the `rarely-updated-projects` role. Projects in either of these two +//! roles are safe from compromise as their keys are offline. Since the keys used for the above +//! operation are kept offline, this is periodic, manual process. +//! +//! ## Snapshot & Timestamp +//! +//! In a community repository, these two keys need to be kept online and will be used to sign new +//! metadata on every update. + +#![deny(missing_docs)] +#![allow( + clippy::collapsible_if, + clippy::implicit_hasher, + clippy::let_unit_value, + clippy::new_ret_no_self, + clippy::op_ref, + clippy::too_many_arguments +)] + +pub mod client; +pub mod crypto; +pub mod database; +pub mod error; +pub mod interchange; +pub mod metadata; +pub mod repo_builder; +pub mod repository; +pub mod verify; + +mod format_hex; +mod util; + +pub use crate::database::*; +pub use crate::error::*; diff --git a/vendor/tuf/src/metadata.rs b/vendor/tuf/src/metadata.rs new file mode 100644 index 0000000000..d62671e5c8 --- /dev/null +++ b/vendor/tuf/src/metadata.rs @@ -0,0 +1,3858 @@ +//! TUF metadata. + +use chrono::offset::Utc; +use chrono::{DateTime, Duration}; +use futures_io::AsyncRead; +use serde::de::{Deserialize, DeserializeOwned, Deserializer, Error as DeserializeError}; +use serde::ser::{Error as SerializeError, Serialize, Serializer}; +use serde_derive::{Deserialize, Serialize}; +use std::borrow::{Borrow, Cow}; +use std::collections::{HashMap, HashSet}; +use std::fmt::{self, Debug, Display}; +use std::marker::PhantomData; +use std::str; + +use crate::crypto::{self, HashAlgorithm, HashValue, KeyId, PrivateKey, PublicKey, Signature}; +use crate::error::Error; +use crate::interchange::cjson::shims; +use crate::interchange::DataInterchange; +use crate::Result; + +#[rustfmt::skip] +static PATH_ILLEGAL_COMPONENTS: &[&str] = &[ + ".", // current dir + "..", // parent dir + // TODO ? "0", // may translate to nul in windows +]; + +#[rustfmt::skip] +static PATH_ILLEGAL_COMPONENTS_CASE_INSENSITIVE: &[&str] = &[ + // DOS device files + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + "KEYBD$", + "CLOCK$", + "SCREEN$", + "$IDLE$", + "CONFIG$", +]; + +#[rustfmt::skip] +static PATH_ILLEGAL_STRINGS: &[&str] = &[ + ":", // for *nix compatibility + "\\", // for windows compatibility + "<", + ">", + "\"", + "|", + "?", + // control characters, all illegal in FAT + "\u{000}", + "\u{001}", + "\u{002}", + "\u{003}", + "\u{004}", + "\u{005}", + "\u{006}", + "\u{007}", + "\u{008}", + "\u{009}", + "\u{00a}", + "\u{00b}", + "\u{00c}", + "\u{00d}", + "\u{00e}", + "\u{00f}", + "\u{010}", + "\u{011}", + "\u{012}", + "\u{013}", + "\u{014}", + "\u{015}", + "\u{016}", + "\u{017}", + "\u{018}", + "\u{019}", + "\u{01a}", + "\u{01b}", + "\u{01c}", + "\u{01d}", + "\u{01e}", + "\u{01f}", + "\u{07f}", +]; + +fn safe_path(path: &str) -> Result<()> { + if path.is_empty() { + return Err(Error::IllegalArgument("Path cannot be empty".into())); + } + + if path.starts_with('/') { + return Err(Error::IllegalArgument("Cannot start with '/'".into())); + } + + for bad_str in PATH_ILLEGAL_STRINGS { + if path.contains(bad_str) { + return Err(Error::IllegalArgument(format!( + "Path cannot contain {:?}", + bad_str + ))); + } + } + + for component in path.split('/') { + for bad_str in PATH_ILLEGAL_COMPONENTS { + if component == *bad_str { + return Err(Error::IllegalArgument(format!( + "Path cannot have component {:?}", + component + ))); + } + } + + let component_lower = component.to_lowercase(); + for bad_str in PATH_ILLEGAL_COMPONENTS_CASE_INSENSITIVE { + if component_lower.as_str() == *bad_str { + return Err(Error::IllegalArgument(format!( + "Path cannot have component {:?}", + component + ))); + } + } + } + + Ok(()) +} + +/// The TUF role. +#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Role { + /// The root role. + #[serde(rename = "root")] + Root, + + /// The snapshot role. + #[serde(rename = "snapshot")] + Snapshot, + + /// The targets role. + #[serde(rename = "targets")] + Targets, + + /// The timestamp role. + #[serde(rename = "timestamp")] + Timestamp, +} + +impl Role { + /// Check if this role could be associated with a given path. + /// + /// ``` + /// use tuf::metadata::{MetadataPath, Role}; + /// + /// assert!(Role::Root.fuzzy_matches_path(&MetadataPath::root())); + /// assert!(Role::Snapshot.fuzzy_matches_path(&MetadataPath::snapshot())); + /// assert!(Role::Targets.fuzzy_matches_path(&MetadataPath::targets())); + /// assert!(Role::Timestamp.fuzzy_matches_path(&MetadataPath::timestamp())); + /// + /// assert!(!Role::Root.fuzzy_matches_path(&MetadataPath::snapshot())); + /// assert!(!Role::Root.fuzzy_matches_path(&MetadataPath::new("wat").unwrap())); + /// ``` + pub fn fuzzy_matches_path(&self, path: &MetadataPath) -> bool { + match *self { + Role::Root if &path.0 == "root" => true, + Role::Snapshot if &path.0 == "snapshot" => true, + Role::Timestamp if &path.0 == "timestamp" => true, + Role::Targets if &path.0 == "targets" => true, + Role::Targets if !&["root", "snapshot", "targets"].contains(&path.0.as_ref()) => true, + _ => false, + } + } + + /// Return the name of the role. + pub fn name(&self) -> &'static str { + match *self { + Role::Root => "root", + Role::Snapshot => "snapshot", + Role::Targets => "targets", + Role::Timestamp => "timestamp", + } + } +} + +impl Display for Role { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.name()) + } +} + +/// Enum used for addressing versioned TUF metadata. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub enum MetadataVersion { + /// The metadata is unversioned. This is the latest version of the metadata. + None, + /// The metadata is addressed by a specific version number. + Number(u64), +} + +impl Display for MetadataVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MetadataVersion::None => f.write_str("none"), + MetadataVersion::Number(version) => write!(f, "{}", version), + } + } +} + +impl MetadataVersion { + /// Converts this struct into the string used for addressing metadata. + pub fn prefix(&self) -> String { + match *self { + MetadataVersion::None => String::new(), + MetadataVersion::Number(ref x) => format!("{}.", x), + } + } +} + +/// Top level trait used for role metadata. +pub trait Metadata: Debug + PartialEq + Serialize + DeserializeOwned { + /// The role associated with the metadata. + const ROLE: Role; + + /// The version number. + fn version(&self) -> u64; + + /// An immutable reference to the metadata's expiration `DateTime`. + fn expires(&self) -> &DateTime; +} + +/// Unverified raw metadata with attached signatures and type information identifying the +/// metadata's type and serialization format. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawSignedMetadata { + bytes: Vec, + _marker: PhantomData<(D, M)>, +} + +impl RawSignedMetadata +where + D: DataInterchange, + M: Metadata, +{ + /// Create a new [`RawSignedMetadata`] using the provided `bytes`. + pub fn new(bytes: Vec) -> Self { + Self { + bytes, + _marker: PhantomData, + } + } + + /// Access this metadata's inner raw bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + /// Parse this metadata. + /// + /// **WARNING**: This does not verify signatures, so it exposes users to potential parser + /// exploits. + pub fn parse_untrusted(&self) -> Result> { + D::from_slice(&self.bytes) + } +} + +/// A collection of [RawSignedMetadata] that describes the metadata at one +/// commit. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RawSignedMetadataSet { + root: Option>, + targets: Option>, + snapshot: Option>, + timestamp: Option>, +} + +impl RawSignedMetadataSet { + /// Returns a reference to the built root metadata, if any. + pub fn root(&self) -> Option<&RawSignedMetadata> { + self.root.as_ref() + } + + /// Returns a reference to the built targets metadata, if any. + pub fn targets(&self) -> Option<&RawSignedMetadata> { + self.targets.as_ref() + } + + /// Returns a reference to the built snapshot metadata, if any. + pub fn snapshot(&self) -> Option<&RawSignedMetadata> { + self.snapshot.as_ref() + } + + /// Returns a reference to the built timestamp metadata, if any. + pub fn timestamp(&self) -> Option<&RawSignedMetadata> { + self.timestamp.as_ref() + } +} + +/// Builder for [RawSignedMetadataSet]. +#[derive(Default)] +pub struct RawSignedMetadataSetBuilder +where + D: DataInterchange, +{ + metadata: RawSignedMetadataSet, +} + +impl RawSignedMetadataSetBuilder +where + D: DataInterchange, +{ + /// Create a new [RawSignedMetadataSetBuilder]. + pub fn new() -> Self { + Self { + metadata: RawSignedMetadataSet { + root: None, + targets: None, + snapshot: None, + timestamp: None, + }, + } + } + + /// Set or replace the root metadata. + pub fn root(mut self, root: RawSignedMetadata) -> Self { + self.metadata.root = Some(root); + self + } + + /// Set or replace the targets metadata. + pub fn targets(mut self, targets: RawSignedMetadata) -> Self { + self.metadata.targets = Some(targets); + self + } + + /// Set or replace the snapshot metadata. + pub fn snapshot(mut self, snapshot: RawSignedMetadata) -> Self { + self.metadata.snapshot = Some(snapshot); + self + } + + /// Set or replace the timestamp metadata. + pub fn timestamp(mut self, timestamp: RawSignedMetadata) -> Self { + self.metadata.timestamp = Some(timestamp); + self + } + + /// Return a [RawSignedMetadataSet]. + pub fn build(self) -> RawSignedMetadataSet { + self.metadata + } +} + +/// Helper to construct `SignedMetadata`. +#[derive(Debug, Clone)] +pub struct SignedMetadataBuilder +where + D: DataInterchange, +{ + signatures: HashMap, + metadata: D::RawData, + metadata_bytes: Vec, + _marker: PhantomData, +} + +impl SignedMetadataBuilder +where + D: DataInterchange, + M: Metadata, +{ + /// Create a new `SignedMetadataBuilder` from a given `Metadata`. + pub fn from_metadata(metadata: &M) -> Result { + let metadata = D::serialize(metadata)?; + Self::from_raw_metadata(metadata) + } + + /// Create a new `SignedMetadataBuilder` from manually serialized metadata to be signed. + /// Returns an error if `metadata` cannot be parsed into `M`. + pub fn from_raw_metadata(metadata: D::RawData) -> Result { + let _ensure_metadata_parses: M = D::deserialize(&metadata)?; + let metadata_bytes = D::canonicalize(&metadata)?; + Ok(Self { + signatures: HashMap::new(), + metadata, + metadata_bytes, + _marker: PhantomData, + }) + } + + /// Sign the metadata using the given `private_key`, replacing any existing signatures with the + /// same `KeyId`. + /// + /// **WARNING**: You should never have multiple TUF private keys on the same machine, so if + /// you're using this to append several signatures at once, you are doing something wrong. The + /// preferred method is to generate your copy of the metadata locally and use + /// `SignedMetadata::merge_signatures` to perform the "append" operations. + pub fn sign(mut self, private_key: &dyn PrivateKey) -> Result { + let sig = private_key.sign(&self.metadata_bytes)?; + let _ = self.signatures.insert(sig.key_id().clone(), sig); + Ok(self) + } + + /// Construct a new `SignedMetadata` using the included signatures, sorting the signatures by + /// `KeyId`. + pub fn build(self) -> SignedMetadata { + let mut signatures = self + .signatures + .into_iter() + .map(|(_k, v)| v) + .collect::>(); + signatures.sort_unstable_by(|a, b| a.key_id().cmp(b.key_id())); + + SignedMetadata { + signatures, + metadata: self.metadata, + _marker: PhantomData, + } + } +} + +/// Serialized metadata with attached unverified signatures. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SignedMetadata +where + D: DataInterchange, +{ + signatures: Vec, + #[serde(rename = "signed")] + metadata: D::RawData, + #[serde(skip_serializing, skip_deserializing)] + _marker: PhantomData, +} + +impl SignedMetadata +where + D: DataInterchange, + M: Metadata, +{ + /// Create a new `SignedMetadata`. The supplied private key is used to sign the canonicalized + /// bytes of the provided metadata with the provided scheme. + /// + /// ``` + /// # use chrono::prelude::*; + /// # use tuf::crypto::{Ed25519PrivateKey, PrivateKey, SignatureScheme, HashAlgorithm}; + /// # use tuf::interchange::Json; + /// # use tuf::metadata::{SignedMetadata, SnapshotMetadataBuilder}; + /// # + /// # let key: &[u8] = include_bytes!("../tests/ed25519/ed25519-1.pk8.der"); + /// let key = Ed25519PrivateKey::from_pkcs8(&key).unwrap(); + /// + /// let snapshot = SnapshotMetadataBuilder::new().build().unwrap(); + /// SignedMetadata::::new(&snapshot, &key).unwrap(); + /// ``` + pub fn new(metadata: &M, private_key: &dyn PrivateKey) -> Result { + let raw = D::serialize(metadata)?; + let bytes = D::canonicalize(&raw)?; + let sig = private_key.sign(&bytes)?; + Ok(Self { + signatures: vec![sig], + metadata: raw, + _marker: PhantomData, + }) + } + + /// Serialize this metadata to canonical bytes suitable for serialization. Note that this + /// method is only intended to serialize signed metadata generated by this crate, not to + /// re-serialize metadata that was originally obtained from a remote source. + /// + /// TUF metadata hashes are on the raw bytes of the metadata, so it is not guaranteed that the + /// hash of the returned bytes will match a hash included in, for example, a snapshot metadata + /// file, as: + /// * Parsing metadata removes unknown fields, which would not be included in the returned + /// bytes, + /// * DataInterchange implementations only guarantee the bytes are canonical for the purpose of + /// a signature. Metadata obtained from a remote source may have included different whitespace + /// or ordered fields in a way that is not preserved when parsing that metadata. + pub fn to_raw(&self) -> Result> { + let bytes = D::canonicalize(&D::serialize(self)?)?; + Ok(RawSignedMetadata::new(bytes)) + } + + /// Append a signature to this signed metadata. Will overwrite signature by keys with the same + /// ID. + /// + /// **WARNING**: You should never have multiple TUF private keys on the same machine, so if + /// you're using this to append several signatures at once, you are doing something wrong. The + /// preferred method is to generate your copy of the metadata locally and use `merge_signatures` + /// to perform the "append" operations. + /// + /// ``` + /// # use chrono::prelude::*; + /// # use tuf::crypto::{Ed25519PrivateKey, PrivateKey, SignatureScheme, HashAlgorithm}; + /// # use tuf::interchange::Json; + /// # use tuf::metadata::{SignedMetadata, SnapshotMetadataBuilder}; + /// # + /// let key_1: &[u8] = include_bytes!("../tests/ed25519/ed25519-1.pk8.der"); + /// let key_1 = Ed25519PrivateKey::from_pkcs8(&key_1).unwrap(); + /// + /// // Note: This is for demonstration purposes only. + /// // You should never have multiple private keys on the same device. + /// let key_2: &[u8] = include_bytes!("../tests/ed25519/ed25519-2.pk8.der"); + /// let key_2 = Ed25519PrivateKey::from_pkcs8(&key_2).unwrap(); + /// + /// let snapshot = SnapshotMetadataBuilder::new().build().unwrap(); + /// let mut snapshot = SignedMetadata::::new(&snapshot, &key_1).unwrap(); + /// + /// snapshot.add_signature(&key_2).unwrap(); + /// assert_eq!(snapshot.signatures().len(), 2); + /// + /// snapshot.add_signature(&key_2).unwrap(); + /// assert_eq!(snapshot.signatures().len(), 2); + /// ``` + pub fn add_signature(&mut self, private_key: &dyn PrivateKey) -> Result<()> { + let bytes = D::canonicalize(&self.metadata)?; + let sig = private_key.sign(&bytes)?; + self.signatures + .retain(|s| s.key_id() != private_key.public().key_id()); + self.signatures.push(sig); + self.signatures.sort(); + Ok(()) + } + + /// Merge the singatures from `other` into `self` if and only if + /// `self.as_ref() == other.as_ref()`. If `self` and `other` contain signatures from the same + /// key ID, then the signatures from `self` will replace the signatures from `other`. + pub fn merge_signatures(&mut self, other: &Self) -> Result<()> { + if self.metadata != other.metadata { + return Err(Error::IllegalArgument( + "Attempted to merge unequal metadata".into(), + )); + } + + let key_ids = self + .signatures + .iter() + .map(|s| s.key_id().clone()) + .collect::>(); + + self.signatures.extend( + other + .signatures + .iter() + .filter(|s| !key_ids.contains(s.key_id())) + .cloned(), + ); + self.signatures.sort(); + + Ok(()) + } + + /// An immutable reference to the signatures. + pub fn signatures(&self) -> &[Signature] { + &self.signatures + } + + /// Parse the version number of this metadata without verifying signatures. + /// + /// This operation is generally unsafe to do with metadata obtained from an untrusted source, + /// but rolling forward to the most recent root.json requires using the version number of the + /// latest root.json. + pub(crate) fn parse_version_untrusted(&self) -> Result { + #[derive(Deserialize)] + pub struct MetadataVersion { + version: u64, + } + + let meta: MetadataVersion = D::deserialize(&self.metadata)?; + Ok(meta.version) + } + + /// Parse this metadata without verifying signatures. + /// + /// This operation is not safe to do with metadata obtained from an untrusted source. + pub fn assume_valid(&self) -> Result { + D::deserialize(&self.metadata) + } +} + +/// Helper to construct `RootMetadata`. +pub struct RootMetadataBuilder { + version: u64, + expires: DateTime, + consistent_snapshot: bool, + keys: HashMap, + root_threshold: u32, + root_key_ids: HashSet, + snapshot_threshold: u32, + snapshot_key_ids: HashSet, + targets_threshold: u32, + targets_key_ids: HashSet, + timestamp_threshold: u32, + timestamp_key_ids: HashSet, +} + +impl RootMetadataBuilder { + /// Create a new `RootMetadataBuilder`. It defaults to: + /// + /// * version: 1, + /// * expires: 365 days from the current time. + /// * consistent snapshot: true + /// * role thresholds: 1 + pub fn new() -> Self { + RootMetadataBuilder { + version: 1, + expires: Utc::now() + Duration::days(365), + consistent_snapshot: true, + keys: HashMap::new(), + root_threshold: 1, + root_key_ids: HashSet::new(), + snapshot_threshold: 1, + snapshot_key_ids: HashSet::new(), + targets_threshold: 1, + targets_key_ids: HashSet::new(), + timestamp_threshold: 1, + timestamp_key_ids: HashSet::new(), + } + } + + /// Set the version number for this metadata. + pub fn version(mut self, version: u64) -> Self { + self.version = version; + self + } + + /// Set the time this metadata expires. + pub fn expires(mut self, expires: DateTime) -> Self { + self.expires = expires; + self + } + + /// Set this metadata to have a consistent snapshot. + pub fn consistent_snapshot(mut self, consistent_snapshot: bool) -> Self { + self.consistent_snapshot = consistent_snapshot; + self + } + + /// Set the root threshold. + pub fn root_threshold(mut self, threshold: u32) -> Self { + self.root_threshold = threshold; + self + } + + /// Add a root public key. + pub fn root_key(mut self, public_key: PublicKey) -> Self { + let key_id = public_key.key_id().clone(); + self.keys.insert(key_id.clone(), public_key); + self.root_key_ids.insert(key_id); + self + } + + /// Set the snapshot threshold. + pub fn snapshot_threshold(mut self, threshold: u32) -> Self { + self.snapshot_threshold = threshold; + self + } + + /// Add a snapshot public key. + pub fn snapshot_key(mut self, public_key: PublicKey) -> Self { + let key_id = public_key.key_id().clone(); + self.keys.insert(key_id.clone(), public_key); + self.snapshot_key_ids.insert(key_id); + self + } + + /// Set the targets threshold. + pub fn targets_threshold(mut self, threshold: u32) -> Self { + self.targets_threshold = threshold; + self + } + + /// Add a targets public key. + pub fn targets_key(mut self, public_key: PublicKey) -> Self { + let key_id = public_key.key_id().clone(); + self.keys.insert(key_id.clone(), public_key); + self.targets_key_ids.insert(key_id); + self + } + + /// Set the timestamp threshold. + pub fn timestamp_threshold(mut self, threshold: u32) -> Self { + self.timestamp_threshold = threshold; + self + } + + /// Add a timestamp public key. + pub fn timestamp_key(mut self, public_key: PublicKey) -> Self { + let key_id = public_key.key_id().clone(); + self.keys.insert(key_id.clone(), public_key); + self.timestamp_key_ids.insert(key_id); + self + } + + /// Construct a new `RootMetadata`. + pub fn build(self) -> Result { + RootMetadata::new( + self.version, + self.expires, + self.consistent_snapshot, + self.keys, + RoleDefinition::new(self.root_threshold, self.root_key_ids)?, + RoleDefinition::new(self.snapshot_threshold, self.snapshot_key_ids)?, + RoleDefinition::new(self.targets_threshold, self.targets_key_ids)?, + RoleDefinition::new(self.timestamp_threshold, self.timestamp_key_ids)?, + Default::default(), + ) + } + + /// Construct a new `SignedMetadata`. + pub fn signed(self, private_key: &dyn PrivateKey) -> Result> + where + D: DataInterchange, + { + SignedMetadata::new(&self.build()?, private_key) + } +} + +impl Default for RootMetadataBuilder { + fn default() -> Self { + RootMetadataBuilder::new() + } +} + +impl From for RootMetadataBuilder { + fn from(metadata: RootMetadata) -> Self { + RootMetadataBuilder { + version: metadata.version, + expires: metadata.expires, + consistent_snapshot: metadata.consistent_snapshot, + keys: metadata.keys, + root_threshold: metadata.root.threshold, + root_key_ids: metadata.root.key_ids, + snapshot_threshold: metadata.snapshot.threshold, + snapshot_key_ids: metadata.snapshot.key_ids, + targets_threshold: metadata.targets.threshold, + targets_key_ids: metadata.targets.key_ids, + timestamp_threshold: metadata.timestamp.threshold, + timestamp_key_ids: metadata.timestamp.key_ids, + } + } +} + +/// Metadata for the root role. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RootMetadata { + version: u64, + expires: DateTime, + consistent_snapshot: bool, + keys: HashMap, + root: RoleDefinition, + snapshot: RoleDefinition, + targets: RoleDefinition, + timestamp: RoleDefinition, + additional_fields: HashMap, +} + +impl RootMetadata { + /// Create new `RootMetadata`. + pub fn new( + version: u64, + expires: DateTime, + consistent_snapshot: bool, + keys: HashMap, + root: RoleDefinition, + snapshot: RoleDefinition, + targets: RoleDefinition, + timestamp: RoleDefinition, + additional_fields: HashMap, + ) -> Result { + if version < 1 { + return Err(Error::IllegalArgument(format!( + "Metadata version must be greater than zero. Found: {}", + version + ))); + } + + Ok(RootMetadata { + version, + expires, + consistent_snapshot, + keys, + root, + snapshot, + targets, + timestamp, + additional_fields, + }) + } + + /// Whether or not this repository is currently implementing that TUF consistent snapshot + /// feature. + pub fn consistent_snapshot(&self) -> bool { + self.consistent_snapshot + } + + /// An immutable reference to the map of trusted keys. + pub fn keys(&self) -> &HashMap { + &self.keys + } + + /// An iterator over all the trusted root public keys. + pub fn root_keys(&self) -> impl Iterator { + self.root + .key_ids() + .iter() + .filter_map(|key_id| self.keys.get(key_id)) + } + + /// An iterator over all the trusted targets public keys. + pub fn targets_keys(&self) -> impl Iterator { + self.targets + .key_ids() + .iter() + .filter_map(|key_id| self.keys.get(key_id)) + } + + /// An iterator over all the trusted snapshot public keys. + pub fn snapshot_keys(&self) -> impl Iterator { + self.snapshot + .key_ids() + .iter() + .filter_map(|key_id| self.keys.get(key_id)) + } + + /// An iterator over all the trusted timestamp public keys. + pub fn timestamp_keys(&self) -> impl Iterator { + self.timestamp + .key_ids() + .iter() + .filter_map(|key_id| self.keys.get(key_id)) + } + + /// An immutable reference to the root role's definition. + pub fn root(&self) -> &RoleDefinition { + &self.root + } + + /// An immutable reference to the snapshot role's definition. + pub fn snapshot(&self) -> &RoleDefinition { + &self.snapshot + } + + /// An immutable reference to the targets role's definition. + pub fn targets(&self) -> &RoleDefinition { + &self.targets + } + + /// An immutable reference to the timestamp role's definition. + pub fn timestamp(&self) -> &RoleDefinition { + &self.timestamp + } + + /// An immutable reference to any additional fields on the metadata. + pub fn additional_fields(&self) -> &HashMap { + &self.additional_fields + } +} + +impl Metadata for RootMetadata { + const ROLE: Role = Role::Root; + + fn version(&self) -> u64 { + self.version + } + + fn expires(&self) -> &DateTime { + &self.expires + } +} + +impl Serialize for RootMetadata { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + let m = shims::RootMetadata::from(self) + .map_err(|e| SerializeError::custom(format!("{:?}", e)))?; + m.serialize(ser) + } +} + +impl<'de> Deserialize<'de> for RootMetadata { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::RootMetadata = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// The definition of what allows a role to be trusted. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RoleDefinition { + threshold: u32, + key_ids: HashSet, +} + +impl RoleDefinition { + /// Create a new [RoleDefinition] with a given threshold and set of authorized [KeyId]s. + pub fn new(threshold: u32, key_ids: HashSet) -> Result { + if threshold < 1 { + return Err(Error::IllegalArgument(format!("Threshold: {}", threshold))); + } + + if key_ids.is_empty() { + return Err(Error::IllegalArgument( + "Cannot define a role with no associated key IDs".into(), + )); + } + + if (key_ids.len() as u64) < u64::from(threshold) { + return Err(Error::IllegalArgument(format!( + "Cannot have a threshold greater than the number of associated key IDs. {} vs. {}", + threshold, + key_ids.len() + ))); + } + + Ok(RoleDefinition { threshold, key_ids }) + } + + /// The threshold number of signatures required for the role to be trusted. + pub fn threshold(&self) -> u32 { + self.threshold + } + + /// An immutable reference to the set of `KeyID`s that are authorized to sign the role. + pub fn key_ids(&self) -> &HashSet { + &self.key_ids + } +} + +impl Serialize for RoleDefinition { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + shims::RoleDefinition::from(self) + .map_err(|e| SerializeError::custom(format!("{:?}", e)))? + .serialize(ser) + } +} + +impl<'de> Deserialize<'de> for RoleDefinition { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::RoleDefinition = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// Wrapper for a path to metadata. +/// +/// Note: This should **not** contain the file extension. This is automatically added by the +/// library depending on what type of data interchange format is being used. +/// +/// ``` +/// use tuf::metadata::MetadataPath; +/// +/// // right +/// let _ = MetadataPath::new("root"); +/// +/// // wrong +/// let _ = MetadataPath::new("root.json"); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +pub struct MetadataPath(Cow<'static, str>); + +impl MetadataPath { + /// Create a new `MetadataPath` for the Root role. + pub fn root() -> Self { + MetadataPath(Role::Root.name().into()) + } + + /// Create a new `MetadataPath` for the Timestamp role. + pub fn timestamp() -> Self { + MetadataPath(Role::Timestamp.name().into()) + } + + /// Create a new `MetadataPath` for the Snapshot role. + pub fn snapshot() -> Self { + MetadataPath(Role::Snapshot.name().into()) + } + + /// Create a new `MetadataPath` for the targets role. + pub fn targets() -> Self { + MetadataPath(Role::Targets.name().into()) + } + + /// Create a new `MetadataPath` from a `String`. + /// + /// ``` + /// # use tuf::metadata::MetadataPath; + /// assert!(MetadataPath::new("foo").is_ok()); + /// assert!(MetadataPath::new("/foo").is_err()); + /// assert!(MetadataPath::new("../foo").is_err()); + /// assert!(MetadataPath::new("foo/..").is_err()); + /// assert!(MetadataPath::new("foo/../bar").is_err()); + /// assert!(MetadataPath::new("..foo").is_ok()); + /// assert!(MetadataPath::new("foo/..bar").is_ok()); + /// assert!(MetadataPath::new("foo/bar..").is_ok()); + /// ``` + pub fn new>>(path: P) -> Result { + let path = path.into(); + match path.as_ref() { + "root" => Ok(MetadataPath::root()), + "timestamp" => Ok(MetadataPath::timestamp()), + "snapshot" => Ok(MetadataPath::snapshot()), + "targets" => Ok(MetadataPath::targets()), + _ => { + safe_path(&path)?; + Ok(MetadataPath(path)) + } + } + } + + /// Split `MetadataPath` into components that can be joined to create URL paths, Unix paths, or + /// Windows paths. + /// + /// ``` + /// # use tuf::crypto::HashValue; + /// # use tuf::interchange::Json; + /// # use tuf::metadata::{MetadataPath, MetadataVersion}; + /// # + /// let path = MetadataPath::new("foo/bar").unwrap(); + /// assert_eq!(path.components::(MetadataVersion::None), + /// ["foo".to_string(), "bar.json".to_string()]); + /// assert_eq!(path.components::(MetadataVersion::Number(1)), + /// ["foo".to_string(), "1.bar.json".to_string()]); + /// ``` + pub fn components(&self, version: MetadataVersion) -> Vec + where + D: DataInterchange, + { + let mut buf: Vec = self.0.split('/').map(|s| s.to_string()).collect(); + let len = buf.len(); + buf[len - 1] = format!("{}{}.{}", version.prefix(), buf[len - 1], D::extension()); + buf + } +} + +impl From for MetadataPath { + fn from(role: Role) -> MetadataPath { + match role { + Role::Root => MetadataPath::root(), + Role::Timestamp => MetadataPath::timestamp(), + Role::Snapshot => MetadataPath::snapshot(), + Role::Targets => MetadataPath::targets(), + } + } +} + +impl Display for MetadataPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl<'de> Deserialize<'de> for MetadataPath { + fn deserialize>(de: D) -> ::std::result::Result { + let s: String = Deserialize::deserialize(de)?; + MetadataPath::new(s).map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// Helper to construct `TimestampMetadata`. +pub struct TimestampMetadataBuilder { + version: u64, + expires: DateTime, + snapshot: MetadataDescription, +} + +impl TimestampMetadataBuilder { + /// Create a new `TimestampMetadataBuilder` from a given snapshot. It defaults to: + /// + /// * version: 1 + /// * expires: 1 day from the current time. + pub fn from_snapshot( + snapshot: &SignedMetadata, + hash_algs: &[HashAlgorithm], + ) -> Result + where + D: DataInterchange, + { + let raw_snapshot = snapshot.to_raw()?; + let description = MetadataDescription::from_slice( + raw_snapshot.as_bytes(), + snapshot.parse_version_untrusted()?, + hash_algs, + )?; + + Ok(Self::from_metadata_description(description)) + } + + /// Create a new `TimestampMetadataBuilder` from a given + /// `MetadataDescription`. It defaults to: + /// + /// * version: 1 + /// * expires: 1 day from the current time. + pub fn from_metadata_description(description: MetadataDescription) -> Self { + TimestampMetadataBuilder { + version: 1, + expires: Utc::now() + Duration::days(1), + snapshot: description, + } + } + + /// Set the version number for this metadata. + pub fn version(mut self, version: u64) -> Self { + self.version = version; + self + } + + /// Set the time this metadata expires. + pub fn expires(mut self, expires: DateTime) -> Self { + self.expires = expires; + self + } + + /// Construct a new `TimestampMetadata`. + pub fn build(self) -> Result { + TimestampMetadata::new( + self.version, + self.expires, + self.snapshot, + Default::default(), + ) + } + + /// Construct a new `SignedMetadata`. + pub fn signed( + self, + private_key: &dyn PrivateKey, + ) -> Result> + where + D: DataInterchange, + { + SignedMetadata::new(&self.build()?, private_key) + } +} + +/// Metadata for the timestamp role. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TimestampMetadata { + version: u64, + expires: DateTime, + snapshot: MetadataDescription, + additional_fields: HashMap, +} + +impl TimestampMetadata { + /// Create new `TimestampMetadata`. + pub fn new( + version: u64, + expires: DateTime, + snapshot: MetadataDescription, + additional_fields: HashMap, + ) -> Result { + if version < 1 { + return Err(Error::IllegalArgument(format!( + "Metadata version must be greater than zero. Found: {}", + version + ))); + } + + Ok(TimestampMetadata { + version, + expires, + snapshot, + additional_fields, + }) + } + + /// An immutable reference to the snapshot description. + pub fn snapshot(&self) -> &MetadataDescription { + &self.snapshot + } + + /// An immutable reference to any additional fields on the metadata. + pub fn additional_fields(&self) -> &HashMap { + &self.additional_fields + } +} + +impl Metadata for TimestampMetadata { + const ROLE: Role = Role::Timestamp; + + fn version(&self) -> u64 { + self.version + } + + fn expires(&self) -> &DateTime { + &self.expires + } +} + +impl Serialize for TimestampMetadata { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + shims::TimestampMetadata::from(self) + .map_err(|e| SerializeError::custom(format!("{:?}", e)))? + .serialize(ser) + } +} + +impl<'de> Deserialize<'de> for TimestampMetadata { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::TimestampMetadata = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// Description of a piece of metadata, used in verification. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct MetadataDescription { + version: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + length: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + hashes: HashMap, +} + +impl MetadataDescription { + /// Create a `MetadataDescription` from a slice. Size and hashes will be calculated. + pub fn from_slice(buf: &[u8], version: u64, hash_algs: &[HashAlgorithm]) -> Result { + if version < 1 { + return Err(Error::IllegalArgument( + "Version must be greater than zero".into(), + )); + } + + let hashes = if hash_algs.is_empty() { + HashMap::new() + } else { + crypto::calculate_hashes_from_slice(buf, hash_algs)? + }; + + Ok(MetadataDescription { + version, + length: Some(buf.len()), + hashes, + }) + } + + /// Create a new `MetadataDescription`. + pub fn new( + version: u64, + length: Option, + hashes: HashMap, + ) -> Result { + if version < 1 { + return Err(Error::IllegalArgument(format!( + "Metadata version must be greater than zero. Found: {}", + version + ))); + } + + Ok(MetadataDescription { + version, + length, + hashes, + }) + } + + /// The version of the described metadata. + pub fn version(&self) -> u64 { + self.version + } + + /// The length of the described metadata. + pub fn length(&self) -> Option { + self.length + } + + /// An immutable reference to the hashes of the described metadata. + pub fn hashes(&self) -> &HashMap { + &self.hashes + } +} + +impl<'de> Deserialize<'de> for MetadataDescription { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::MetadataDescription = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// Helper to construct `SnapshotMetadata`. +pub struct SnapshotMetadataBuilder { + version: u64, + expires: DateTime, + meta: HashMap, +} + +impl SnapshotMetadataBuilder { + /// Create a new `SnapshotMetadataBuilder`. It defaults to: + /// + /// * version: 1 + /// * expires: 7 days from the current time. + pub fn new() -> Self { + SnapshotMetadataBuilder { + version: 1, + expires: Utc::now() + Duration::days(7), + meta: HashMap::new(), + } + } + + /// Create a new [SnapshotMetadataBuilder] from a given snapshot. It defaults to: + /// + /// * version: 1 + /// * expires: 7 day from the current time. + pub fn from_targets( + targets: &SignedMetadata, + hash_algs: &[HashAlgorithm], + ) -> Result + where + D: DataInterchange, + { + SnapshotMetadataBuilder::new().insert_metadata(targets, hash_algs) + } + + /// Set the version number for this metadata. + pub fn version(mut self, version: u64) -> Self { + self.version = version; + self + } + + /// Set the time this metadata expires. + pub fn expires(mut self, expires: DateTime) -> Self { + self.expires = expires; + self + } + + /// Add metadata to this snapshot metadata using the default path. + pub fn insert_metadata( + self, + metadata: &SignedMetadata, + hash_algs: &[HashAlgorithm], + ) -> Result + where + M: Metadata, + D: DataInterchange, + { + self.insert_metadata_with_path(M::ROLE.name(), metadata, hash_algs) + } + + /// Add metadata to this snapshot metadata using a custom path. + pub fn insert_metadata_with_path( + self, + path: P, + metadata: &SignedMetadata, + hash_algs: &[HashAlgorithm], + ) -> Result + where + P: Into>, + M: Metadata, + D: DataInterchange, + { + let raw_metadata = metadata.to_raw()?; + let description = MetadataDescription::from_slice( + raw_metadata.as_bytes(), + metadata.parse_version_untrusted()?, + hash_algs, + )?; + let path = MetadataPath::new(path)?; + Ok(self.insert_metadata_description(path, description)) + } + + /// Add `MetadataDescription` to this snapshot metadata using a custom path. + pub fn insert_metadata_description( + mut self, + path: MetadataPath, + description: MetadataDescription, + ) -> Self { + self.meta.insert(path, description); + self + } + + /// Construct a new `SnapshotMetadata`. + pub fn build(self) -> Result { + SnapshotMetadata::new(self.version, self.expires, self.meta, Default::default()) + } + + /// Construct a new `SignedMetadata`. + pub fn signed( + self, + private_key: &dyn PrivateKey, + ) -> Result> + where + D: DataInterchange, + { + SignedMetadata::new(&self.build()?, private_key) + } +} + +impl Default for SnapshotMetadataBuilder { + fn default() -> Self { + SnapshotMetadataBuilder::new() + } +} + +impl From for SnapshotMetadataBuilder { + fn from(meta: SnapshotMetadata) -> Self { + SnapshotMetadataBuilder { + version: meta.version, + expires: meta.expires, + meta: meta.meta, + } + } +} + +/// Metadata for the snapshot role. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SnapshotMetadata { + version: u64, + expires: DateTime, + meta: HashMap, + additional_fields: HashMap, +} + +impl SnapshotMetadata { + /// Create new `SnapshotMetadata`. + pub fn new( + version: u64, + expires: DateTime, + meta: HashMap, + additional_fields: HashMap, + ) -> Result { + if version < 1 { + return Err(Error::IllegalArgument(format!( + "Metadata version must be greater than zero. Found: {}", + version + ))); + } + + Ok(SnapshotMetadata { + version, + expires, + meta, + additional_fields, + }) + } + + /// An immutable reference to the metadata paths and descriptions. + pub fn meta(&self) -> &HashMap { + &self.meta + } + + /// An immutable reference to any additional fields on the metadata. + pub fn additional_fields(&self) -> &HashMap { + &self.additional_fields + } +} + +impl Metadata for SnapshotMetadata { + const ROLE: Role = Role::Snapshot; + + fn version(&self) -> u64 { + self.version + } + + fn expires(&self) -> &DateTime { + &self.expires + } +} + +impl Serialize for SnapshotMetadata { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + shims::SnapshotMetadata::from(self) + .map_err(|e| SerializeError::custom(format!("{:?}", e)))? + .serialize(ser) + } +} + +impl<'de> Deserialize<'de> for SnapshotMetadata { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::SnapshotMetadata = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// Wrapper for the virtual path to a target. +#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord, Serialize)] +pub struct TargetPath(String); + +impl TargetPath { + /// Create a new `TargetPath` from a `String`. + /// + /// ``` + /// # use tuf::metadata::TargetPath; + /// assert!(TargetPath::new("foo").is_ok()); + /// assert!(TargetPath::new("/foo").is_err()); + /// assert!(TargetPath::new("../foo").is_err()); + /// assert!(TargetPath::new("foo/..").is_err()); + /// assert!(TargetPath::new("foo/../bar").is_err()); + /// assert!(TargetPath::new("..foo").is_ok()); + /// assert!(TargetPath::new("foo/..bar").is_ok()); + /// assert!(TargetPath::new("foo/bar..").is_ok()); + /// ``` + pub fn new>(path: P) -> Result { + let path = path.into(); + safe_path(&path)?; + Ok(TargetPath(path)) + } + + /// Split `TargetPath` into components that can be joined to create URL paths, Unix + /// paths, or Windows paths. + /// + /// ``` + /// # use tuf::metadata::TargetPath; + /// let path = TargetPath::new("foo/bar").unwrap(); + /// assert_eq!(path.components(), ["foo".to_string(), "bar".to_string()]); + /// ``` + pub fn components(&self) -> Vec { + self.0.split('/').map(|s| s.to_string()).collect() + } + + /// Return whether this path is the child of another path. + /// + /// ``` + /// # use tuf::metadata::TargetPath; + /// let path1 = TargetPath::new("foo").unwrap(); + /// let path2 = TargetPath::new("foo/bar").unwrap(); + /// assert!(!path2.is_child(&path1)); + /// + /// let path1 = TargetPath::new("foo/").unwrap(); + /// let path2 = TargetPath::new("foo/bar").unwrap(); + /// assert!(path2.is_child(&path1)); + /// + /// let path2 = TargetPath::new("foo/bar/baz").unwrap(); + /// assert!(path2.is_child(&path1)); + /// + /// let path2 = TargetPath::new("wat").unwrap(); + /// assert!(!path2.is_child(&path1)) + /// ``` + pub fn is_child(&self, parent: &Self) -> bool { + if !parent.0.ends_with('/') { + return false; + } + + self.0.starts_with(&parent.0) + } + + /// Whether or not the current target is available at the end of the given chain of target + /// paths. For the chain to be valid, each target path in a group must be a child of of all + /// previous groups. + // TODO this is hideous and uses way too much clone/heap but I think recursively, + // so here we are + pub fn matches_chain(&self, parents: &[HashSet]) -> bool { + if parents.is_empty() { + return false; + } + if parents.len() == 1 { + return parents[0].iter().any(|p| p == self || self.is_child(p)); + } + + let new = parents[1..] + .iter() + .map(|group| { + group + .iter() + .filter(|parent| { + parents[0] + .iter() + .any(|p| parent.is_child(p) || parent == &p) + }) + .cloned() + .collect::>() + }) + .collect::>(); + self.matches_chain(&new) + } + + /// Prefix the target path with a hash value to support TUF spec 5.5.2. + pub fn with_hash_prefix(&self, hash: &HashValue) -> Result { + let mut components = self.components(); + + let file_name = components + .pop() + .ok_or_else(|| Error::IllegalArgument("Path cannot be empty".into()))?; + + components.push(format!("{}.{}", hash, file_name)); + + TargetPath::new(components.join("/")) + } + + /// The string value of the path. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Display for TargetPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl<'de> Deserialize<'de> for TargetPath { + fn deserialize>(de: D) -> ::std::result::Result { + let s: String = Deserialize::deserialize(de)?; + TargetPath::new(s).map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +impl Borrow for TargetPath { + fn borrow(&self) -> &str { + self.as_str() + } +} + +/// Description of a target, used in verification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TargetDescription { + length: u64, + hashes: HashMap, + custom: HashMap, +} + +impl TargetDescription { + /// Create a new `TargetDescription`. + /// + /// Note: Creating this manually could lead to errors, and the `from_reader` method is + /// preferred. + pub fn new( + length: u64, + hashes: HashMap, + custom: HashMap, + ) -> Result { + if hashes.is_empty() { + return Err(Error::IllegalArgument( + "Cannot have empty set of hashes".into(), + )); + } + + Ok(TargetDescription { + length, + hashes, + custom, + }) + } + + /// Read the from the given slice and calculate the length and hash values. + /// + /// ``` + /// # use data_encoding::BASE64URL; + /// # use tuf::crypto::{HashAlgorithm,HashValue}; + /// # use tuf::metadata::TargetDescription; + /// # + /// let bytes: &[u8] = b"it was a pleasure to burn"; + /// + /// let target_description = TargetDescription::from_slice( + /// bytes, + /// &[HashAlgorithm::Sha256, HashAlgorithm::Sha512], + /// ).unwrap(); + /// + /// let s = "Rd9zlbzrdWfeL7gnIEi05X-Yv2TCpy4qqZM1N72ZWQs="; + /// let sha256 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); + /// + /// let s ="tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ + /// BU1zKP8GShoJuXEtCf5NkDTCEJgQ=="; + /// let sha512 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); + /// + /// assert_eq!(target_description.length(), bytes.len() as u64); + /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha256), Some(&sha256)); + /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha512), Some(&sha512)); + /// ``` + pub fn from_slice(buf: &[u8], hash_algs: &[HashAlgorithm]) -> Result { + Self::from_slice_with_custom(buf, hash_algs, HashMap::new()) + } + + /// Read the from the given reader and custom metadata and calculate the length and hash + /// values. + /// + /// ``` + /// # use data_encoding::BASE64URL; + /// # use serde_json::Value; + /// # use std::collections::HashMap; + /// # use tuf::crypto::{HashAlgorithm,HashValue}; + /// # use tuf::metadata::TargetDescription; + /// # + /// let bytes: &[u8] = b"it was a pleasure to burn"; + /// + /// let mut custom = HashMap::new(); + /// custom.insert("Hello".into(), "World".into()); + /// + /// let target_description = TargetDescription::from_slice_with_custom( + /// bytes, + /// &[HashAlgorithm::Sha256, HashAlgorithm::Sha512], + /// custom, + /// ).unwrap(); + /// + /// let s = "Rd9zlbzrdWfeL7gnIEi05X-Yv2TCpy4qqZM1N72ZWQs="; + /// let sha256 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); + /// + /// let s ="tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ + /// BU1zKP8GShoJuXEtCf5NkDTCEJgQ=="; + /// let sha512 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); + /// + /// assert_eq!(target_description.length(), bytes.len() as u64); + /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha256), Some(&sha256)); + /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha512), Some(&sha512)); + /// assert_eq!(target_description.custom().get("Hello"), Some(&"World".into())); + /// ``` + pub fn from_slice_with_custom( + buf: &[u8], + hash_algs: &[HashAlgorithm], + custom: HashMap, + ) -> Result { + let hashes = crypto::calculate_hashes_from_slice(buf, hash_algs)?; + Ok(TargetDescription { + length: buf.len() as u64, + hashes, + custom, + }) + } + + /// Read the from the given reader and calculate the length and hash values. + /// + /// ``` + /// # use data_encoding::BASE64URL; + /// # use futures_executor::block_on; + /// # use tuf::crypto::{HashAlgorithm,HashValue}; + /// # use tuf::metadata::TargetDescription; + /// # + /// # block_on(async { + /// let bytes: &[u8] = b"it was a pleasure to burn"; + /// + /// let target_description = TargetDescription::from_reader( + /// bytes, + /// &[HashAlgorithm::Sha256, HashAlgorithm::Sha512], + /// ).await.unwrap(); + /// + /// let s = "Rd9zlbzrdWfeL7gnIEi05X-Yv2TCpy4qqZM1N72ZWQs="; + /// let sha256 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); + /// + /// let s ="tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ + /// BU1zKP8GShoJuXEtCf5NkDTCEJgQ=="; + /// let sha512 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); + /// + /// assert_eq!(target_description.length(), bytes.len() as u64); + /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha256), Some(&sha256)); + /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha512), Some(&sha512)); + /// # }) + /// ``` + pub async fn from_reader(read: R, hash_algs: &[HashAlgorithm]) -> Result + where + R: AsyncRead + Unpin, + { + Self::from_reader_with_custom(read, hash_algs, HashMap::new()).await + } + + /// Read the from the given reader and custom metadata and calculate the length and hash + /// values. + /// + /// ``` + /// # use data_encoding::BASE64URL; + /// # use futures_executor::block_on; + /// # use serde_json::Value; + /// # use std::collections::HashMap; + /// # use tuf::crypto::{HashAlgorithm,HashValue}; + /// # use tuf::metadata::TargetDescription; + /// # + /// # block_on(async { + /// let bytes: &[u8] = b"it was a pleasure to burn"; + /// + /// let mut custom = HashMap::new(); + /// custom.insert("Hello".into(), "World".into()); + /// + /// let target_description = TargetDescription::from_reader_with_custom( + /// bytes, + /// &[HashAlgorithm::Sha256, HashAlgorithm::Sha512], + /// custom, + /// ).await.unwrap(); + /// + /// let s = "Rd9zlbzrdWfeL7gnIEi05X-Yv2TCpy4qqZM1N72ZWQs="; + /// let sha256 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); + /// + /// let s ="tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ + /// BU1zKP8GShoJuXEtCf5NkDTCEJgQ=="; + /// let sha512 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); + /// + /// assert_eq!(target_description.length(), bytes.len() as u64); + /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha256), Some(&sha256)); + /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha512), Some(&sha512)); + /// assert_eq!(target_description.custom().get("Hello"), Some(&"World".into())); + /// }) + /// ``` + pub async fn from_reader_with_custom( + read: R, + hash_algs: &[HashAlgorithm], + custom: HashMap, + ) -> Result + where + R: AsyncRead + Unpin, + { + let (length, hashes) = crypto::calculate_hashes_from_reader(read, hash_algs).await?; + Ok(TargetDescription { + length, + hashes, + custom, + }) + } + + /// The maximum length of the target. + pub fn length(&self) -> u64 { + self.length + } + + /// An immutable reference to the list of calculated hashes. + pub fn hashes(&self) -> &HashMap { + &self.hashes + } + + /// An immutable reference to the custom metadata. + pub fn custom(&self) -> &HashMap { + &self.custom + } +} + +impl Serialize for TargetDescription { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + shims::TargetDescription::from(self).serialize(ser) + } +} + +impl<'de> Deserialize<'de> for TargetDescription { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::TargetDescription = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// Metadata for the targets role. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TargetsMetadata { + version: u64, + expires: DateTime, + targets: HashMap, + delegations: Delegations, + additional_fields: HashMap, +} + +impl TargetsMetadata { + /// Create new `TargetsMetadata`. + pub fn new( + version: u64, + expires: DateTime, + targets: HashMap, + delegations: Delegations, + additional_fields: HashMap, + ) -> Result { + if version < 1 { + return Err(Error::IllegalArgument(format!( + "Metadata version must be greater than zero. Found: {}", + version + ))); + } + + Ok(TargetsMetadata { + version, + expires, + targets, + delegations, + additional_fields, + }) + } + + /// An immutable reference to the descriptions of targets. + pub fn targets(&self) -> &HashMap { + &self.targets + } + + /// An immutable reference to the optional delegations. + pub fn delegations(&self) -> &Delegations { + &self.delegations + } + + /// An immutable reference to any additional fields on the metadata. + pub fn additional_fields(&self) -> &HashMap { + &self.additional_fields + } +} + +impl Metadata for TargetsMetadata { + const ROLE: Role = Role::Targets; + + fn version(&self) -> u64 { + self.version + } + + fn expires(&self) -> &DateTime { + &self.expires + } +} + +impl Serialize for TargetsMetadata { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + shims::TargetsMetadata::from(self) + .map_err(|e| SerializeError::custom(format!("{:?}", e)))? + .serialize(ser) + } +} + +impl<'de> Deserialize<'de> for TargetsMetadata { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::TargetsMetadata = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// Helper to construct `TargetsMetadata`. +pub struct TargetsMetadataBuilder { + version: u64, + expires: DateTime, + targets: HashMap, + delegations: Option, +} + +impl TargetsMetadataBuilder { + /// Create a new `TargetsMetadataBuilder`. It defaults to: + /// + /// * version: 1 + /// * expires: 90 days from the current time. + pub fn new() -> Self { + TargetsMetadataBuilder { + version: 1, + expires: Utc::now() + Duration::days(90), + targets: HashMap::new(), + delegations: None, + } + } + + /// Set the version number for this metadata. + pub fn version(mut self, version: u64) -> Self { + self.version = version; + self + } + + /// Set the time this metadata expires. + pub fn expires(mut self, expires: DateTime) -> Self { + self.expires = expires; + self + } + + /// Add target to the target metadata. + pub fn insert_target_from_slice( + self, + path: TargetPath, + buf: &[u8], + hash_algs: &[HashAlgorithm], + ) -> Result { + let description = TargetDescription::from_slice(buf, hash_algs)?; + Ok(self.insert_target_description(path, description)) + } + + /// Add target to the target metadata. + pub async fn insert_target_from_reader( + self, + path: TargetPath, + read: R, + hash_algs: &[HashAlgorithm], + ) -> Result + where + R: AsyncRead + Unpin, + { + let description = TargetDescription::from_reader(read, hash_algs).await?; + Ok(self.insert_target_description(path, description)) + } + + /// Add `TargetDescription` to this target metadata target description. + pub fn insert_target_description( + mut self, + path: TargetPath, + description: TargetDescription, + ) -> Self { + self.targets.insert(path, description); + self + } + + /// Add `Delegations` to this target metadata. + pub fn delegations(mut self, delegations: Delegations) -> Self { + self.delegations = Some(delegations); + self + } + + /// Construct a new `TargetsMetadata`. + pub fn build(self) -> Result { + TargetsMetadata::new( + self.version, + self.expires, + self.targets, + self.delegations.unwrap_or_default(), + Default::default(), + ) + } + + /// Construct a new `SignedMetadata`. + pub fn signed( + self, + private_key: &dyn PrivateKey, + ) -> Result> + where + D: DataInterchange, + { + SignedMetadata::new(&self.build()?, private_key) + } +} + +impl Default for TargetsMetadataBuilder { + fn default() -> Self { + TargetsMetadataBuilder::new() + } +} + +/// Wrapper to described a collections of delegations. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Delegations { + keys: HashMap, + roles: Vec, +} + +impl Delegations { + /// Return a [DelegationsBuilder]. + pub fn builder() -> DelegationsBuilder { + DelegationsBuilder::new() + } + + // TODO check all keys are used + // TODO check all roles have their ID in the set of keys + /// Create a new `Delegations` wrapper from the given set of trusted keys and roles. + pub fn new(keys: HashMap, roles: Vec) -> Result { + if roles.len() + != roles + .iter() + .map(|r| &r.name) + .collect::>() + .len() + { + return Err(Error::IllegalArgument( + "Cannot have duplicated roles in delegations.".into(), + )); + } + + Ok(Delegations { keys, roles }) + } + + /// Return if this delegation is empty. + pub fn is_empty(&self) -> bool { + self.keys.is_empty() && self.roles.is_empty() + } + + /// An immutable reference to the keys used for this set of delegations. + pub fn keys(&self) -> &HashMap { + &self.keys + } + + /// An immutable reference to the delegated roles. + pub fn roles(&self) -> &Vec { + &self.roles + } +} + +impl Serialize for Delegations { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + shims::Delegations::from(self).serialize(ser) + } +} + +impl<'de> Deserialize<'de> for Delegations { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::Delegations = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// A builder for [Delegations]. +#[derive(Default)] +pub struct DelegationsBuilder { + keys: HashMap, + roles: Vec, + role_index: HashMap, +} + +impl DelegationsBuilder { + /// Create a new [DelegationsBuilder]. + pub fn new() -> Self { + Self { + keys: HashMap::new(), + roles: vec![], + role_index: HashMap::new(), + } + } + + /// Include this key in the delegation [PublicKey] set. + pub fn key(mut self, key: PublicKey) -> Self { + self.keys.insert(key.key_id().clone(), key); + self + } + + /// Add a [Delegation]. + pub fn role(mut self, delegation: Delegation) -> Self { + // The delegation list is ordered and unique by role name, so check if we should overwrite + // the old delegation. + if let Some(idx) = self.role_index.get(&delegation.name) { + self.roles[*idx] = delegation; + } else { + self.role_index + .insert(delegation.name.clone(), self.roles.len()); + + self.roles.push(delegation); + } + + self + } + + /// Construct a new [Delegations]. + pub fn build(self) -> Result { + Delegations::new(self.keys, self.roles) + } +} + +/// A delegated targets role. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Delegation { + name: MetadataPath, + terminating: bool, + threshold: u32, + key_ids: HashSet, + paths: HashSet, +} + +impl Delegation { + /// Create a new [DelegationBuilder] for a delegation named `role`. + pub fn builder(role: MetadataPath) -> DelegationBuilder { + DelegationBuilder::new(role) + } + + /// Create a new delegation. + pub fn new( + name: MetadataPath, + terminating: bool, + threshold: u32, + key_ids: HashSet, + paths: HashSet, + ) -> Result { + if key_ids.is_empty() { + return Err(Error::IllegalArgument("Cannot have empty key IDs".into())); + } + + if paths.is_empty() { + return Err(Error::IllegalArgument("Cannot have empty paths".into())); + } + + if threshold < 1 { + return Err(Error::IllegalArgument("Cannot have threshold < 1".into())); + } + + if (key_ids.len() as u64) < u64::from(threshold) { + return Err(Error::IllegalArgument( + "Cannot have threshold less than number of keys".into(), + )); + } + + Ok(Delegation { + name, + terminating, + threshold, + key_ids, + paths, + }) + } + + /// An immutable reference to the delegations's metadata path (role). + pub fn name(&self) -> &MetadataPath { + &self.name + } + + /// Whether or not this delegation is terminating. + pub fn terminating(&self) -> bool { + self.terminating + } + + /// An immutable reference to the delegations's trusted key IDs. + pub fn key_ids(&self) -> &HashSet { + &self.key_ids + } + + /// The delegation's threshold. + pub fn threshold(&self) -> u32 { + self.threshold + } + + /// An immutable reference to the delegation's authorized paths. + pub fn paths(&self) -> &HashSet { + &self.paths + } +} + +impl Serialize for Delegation { + fn serialize(&self, ser: S) -> ::std::result::Result + where + S: Serializer, + { + shims::Delegation::from(self).serialize(ser) + } +} + +impl<'de> Deserialize<'de> for Delegation { + fn deserialize>(de: D) -> ::std::result::Result { + let intermediate: shims::Delegation = Deserialize::deserialize(de)?; + intermediate + .try_into() + .map_err(|e| DeserializeError::custom(format!("{:?}", e))) + } +} + +/// A builder for [Delegation]. +pub struct DelegationBuilder { + role: MetadataPath, + terminating: bool, + threshold: u32, + key_ids: HashSet, + paths: HashSet, +} + +impl DelegationBuilder { + /// Create a new [DelegationBuilder] for a delegation named `role`. + pub fn new(role: MetadataPath) -> Self { + Self { + role, + terminating: false, + threshold: 1, + key_ids: HashSet::new(), + paths: HashSet::new(), + } + } + + /// The threshold number of signatures required for the delegation to be trusted. + pub fn threshold(mut self, threshold: u32) -> Self { + self.threshold = threshold; + self + } + + /// This delegation can be signed by this [PublicKey]. + pub fn key(mut self, key: &PublicKey) -> Self { + self.key_ids.insert(key.key_id().clone()); + self + } + + /// This delegation can be signed by this [KeyId]. + pub fn key_id(mut self, key_id: KeyId) -> Self { + self.key_ids.insert(key_id); + self + } + + /// Delegate `path` to this delegation. + pub fn delegate_path(mut self, path: TargetPath) -> Self { + self.paths.insert(path); + self + } + + /// Construct the [Delegation]. + pub fn build(self) -> Result { + Delegation::new( + self.role, + self.terminating, + self.threshold, + self.key_ids, + self.paths, + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::crypto::Ed25519PrivateKey; + use crate::interchange::Json; + use crate::verify::verify_signatures; + use assert_matches::assert_matches; + use chrono::prelude::*; + use futures_executor::block_on; + use maplit::{hashmap, hashset}; + use pretty_assertions::assert_eq; + use serde_json::json; + use std::str::FromStr; + + const ED25519_1_PK8: &[u8] = include_bytes!("../tests/ed25519/ed25519-1.pk8.der"); + const ED25519_2_PK8: &[u8] = include_bytes!("../tests/ed25519/ed25519-2.pk8.der"); + const ED25519_3_PK8: &[u8] = include_bytes!("../tests/ed25519/ed25519-3.pk8.der"); + const ED25519_4_PK8: &[u8] = include_bytes!("../tests/ed25519/ed25519-4.pk8.der"); + + #[test] + fn no_pardir_in_target_path() { + let bad_paths = &[ + "..", + "../some/path", + "../some/path/", + "some/../path", + "some/../path/..", + ]; + + for path in bad_paths.iter() { + assert!(safe_path(path).is_err()); + assert!(TargetPath::new(path.to_string()).is_err()); + assert!(MetadataPath::new(path.to_string()).is_err()); + assert!(TargetPath::new(path.to_string()).is_err()); + } + } + + #[test] + fn allow_asterisk_in_target_path() { + let good_paths = &[ + "*", + "*/some/path", + "*/some/path/", + "some/*/path", + "some/*/path/*", + ]; + + for path in good_paths.iter() { + assert!(safe_path(path).is_ok()); + assert!(TargetPath::new(path.to_string()).is_ok()); + assert!(MetadataPath::new(path.to_string()).is_ok()); + assert!(TargetPath::new(path.to_string()).is_ok()); + } + } + + #[test] + fn path_matches_chain() { + let test_cases: &[(bool, &str, &[&[&str]])] = &[ + // simplest case + (true, "foo", &[&["foo"]]), + // direct delegation case + (true, "foo", &[&["foo"], &["foo"]]), + // is a dir + (false, "foo", &[&["foo/"]]), + // target not in last position + (false, "foo", &[&["foo"], &["bar"]]), + // target nested + (true, "foo/bar", &[&["foo/"], &["foo/bar"]]), + // target illegally nested + (false, "foo/bar", &[&["baz/"], &["foo/bar"]]), + // target illegally deeply nested + ( + false, + "foo/bar/baz", + &[&["foo/"], &["foo/quux/"], &["foo/bar/baz"]], + ), + // empty + (false, "foo", &[&[]]), + // empty 2 + (false, "foo", &[&[], &["foo"]]), + // empty 3 + (false, "foo", &[&["foo"], &[]]), + ]; + + for case in test_cases { + let expected = case.0; + let target = TargetPath::new(case.1).unwrap(); + let parents = case + .2 + .iter() + .map(|group| { + group + .iter() + .map(|p| TargetPath::new(p.to_string()).unwrap()) + .collect::>() + }) + .collect::>(); + println!( + "CASE: expect: {} path: {:?} parents: {:?}", + expected, target, parents + ); + assert_eq!(target.matches_chain(&parents), expected); + } + } + + #[test] + fn serde_target_path() { + let s = "foo/bar"; + let t = serde_json::from_str::(&format!("\"{}\"", s)).unwrap(); + assert_eq!(t.to_string().as_str(), s); + assert_eq!(serde_json::to_value(t).unwrap(), json!("foo/bar")); + } + + #[test] + fn serde_metadata_path() { + let s = "foo/bar"; + let m = serde_json::from_str::(&format!("\"{}\"", s)).unwrap(); + assert_eq!(m.to_string().as_str(), s); + assert_eq!(serde_json::to_value(m).unwrap(), json!("foo/bar")); + } + + #[test] + fn serde_target_description() { + let s: &[u8] = b"from water does all life begin"; + let description = TargetDescription::from_slice(s, &[HashAlgorithm::Sha256]).unwrap(); + let jsn_str = serde_json::to_string(&description).unwrap(); + let jsn = json!({ + "length": 30, + "hashes": { + "sha256": "fc5d745c712bc86ea9a31264dac0c956eeb53857f677eed05829\ + bb71013cae18", + }, + }); + let parsed_str: TargetDescription = serde_json::from_str(&jsn_str).unwrap(); + let parsed_jsn: TargetDescription = serde_json::from_value(jsn).unwrap(); + assert_eq!(parsed_str, parsed_jsn); + } + + #[test] + fn serde_role_definition() { + // keyid ordering must be preserved. + let keyids = hashset![ + KeyId::from_str("40e35e8f6003ab90d104710cf88901edab931597401f91c19eeb366060ab3d53") + .unwrap(), + KeyId::from_str("01892c662c8cd79fab20edec21de1dcb8b75d9353103face7fe086ff5c0098e4") + .unwrap(), + KeyId::from_str("4750eaf6878740780d6f97b12dbad079fb012bec88c78de2c380add56d3f51db") + .unwrap(), + ]; + let role_def = RoleDefinition::new(3, keyids).unwrap(); + let jsn = json!({ + "threshold": 3, + "keyids": [ + "01892c662c8cd79fab20edec21de1dcb8b75d9353103face7fe086ff5c0098e4", + "40e35e8f6003ab90d104710cf88901edab931597401f91c19eeb366060ab3d53", + "4750eaf6878740780d6f97b12dbad079fb012bec88c78de2c380add56d3f51db", + ], + }); + let encoded = serde_json::to_value(&role_def).unwrap(); + assert_eq!(encoded, jsn); + let decoded: RoleDefinition = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, role_def); + } + + #[test] + fn serde_invalid_role_definitions() { + let jsn = json!({ + "threshold": 0, + "keyids": [ + "01892c662c8cd79fab20edec21de1dcb8b75d9353103face7fe086ff5c0098e4", + "4750eaf6878740780d6f97b12dbad079fb012bec88c78de2c380add56d3f51db", + ], + }); + assert!(serde_json::from_value::(jsn).is_err()); + + let jsn = json!({ + "threshold": -1, + "keyids": [ + "01892c662c8cd79fab20edec21de1dcb8b75d9353103face7fe086ff5c0098e4", + "4750eaf6878740780d6f97b12dbad079fb012bec88c78de2c380add56d3f51db", + ], + }); + assert!(serde_json::from_value::(jsn).is_err()); + } + + #[test] + fn serde_root_metadata() { + let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let snapshot_key = Ed25519PrivateKey::from_pkcs8(ED25519_2_PK8).unwrap(); + let targets_key = Ed25519PrivateKey::from_pkcs8(ED25519_3_PK8).unwrap(); + let timestamp_key = Ed25519PrivateKey::from_pkcs8(ED25519_4_PK8).unwrap(); + + let root = RootMetadataBuilder::new() + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .root_key(root_key.public().clone()) + .snapshot_key(snapshot_key.public().clone()) + .targets_key(targets_key.public().clone()) + .timestamp_key(timestamp_key.public().clone()) + .build() + .unwrap(); + + let jsn = json!({ + "_type": "root", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "consistent_snapshot": true, + "keys": { + "09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": "1410ae3053aa70bbfa98428a879d64d3002a3578f7dfaaeb1cb0764e860f7e0b", + }, + }, + "40e35e8f6003ab90d104710cf88901edab931597401f91c19eeb366060ab3d53": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": "166376c90a7f717d027056272f361c252fb050bed1a067ff2089a0302fbab73d", + }, + }, + "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6", + }, + }, + "fd7b7741686fa44903f1e4b61d7db869939f402b4acedc044767922c7d309983": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": "68d9ecb387371005a8eb8e60105305c34356a8fcd859d7fef3cc228bf2b2b3b2", + }, + } + }, + "roles": { + "root": { + "threshold": 1, + "keyids": ["a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a"], + }, + "snapshot": { + "threshold": 1, + "keyids": ["fd7b7741686fa44903f1e4b61d7db869939f402b4acedc044767922c7d309983"], + }, + "targets": { + "threshold": 1, + "keyids": ["40e35e8f6003ab90d104710cf88901edab931597401f91c19eeb366060ab3d53"], + }, + "timestamp": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"], + }, + }, + }); + + let encoded = serde_json::to_value(&root).unwrap(); + assert_eq!(encoded, jsn); + let decoded: RootMetadata = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, root); + } + + fn jsn_root_metadata_without_keyid_hash_algos() -> serde_json::Value { + json!({ + "_type": "root", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "consistent_snapshot": false, + "keys": { + "12435b260b6172bd750aeb102f54a347c56b109e0524ab1f144593c07af66356": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyval": { + "public": "68d9ecb387371005a8eb8e60105305c34356a8fcd859d7fef3cc228bf2b2b3b2", + }, + }, + "3af6b427c05274532231760f39d81212fdf8ac1a9f8fddf12722623ccec02fec": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyval": { + "public": "1410ae3053aa70bbfa98428a879d64d3002a3578f7dfaaeb1cb0764e860f7e0b", + }, + }, + "b9c336828063cf4fe5348e9fe2d86827c7b3104a76b1f4484a56bbef1ef08cfb": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyval": { + "public": "166376c90a7f717d027056272f361c252fb050bed1a067ff2089a0302fbab73d", + }, + }, + "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyval": { + "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6", + }, + } + }, + "roles": { + "root": { + "threshold": 1, + "keyids": ["e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554"], + }, + "snapshot": { + "threshold": 1, + "keyids": ["12435b260b6172bd750aeb102f54a347c56b109e0524ab1f144593c07af66356"], + }, + "targets": { + "threshold": 1, + "keyids": ["b9c336828063cf4fe5348e9fe2d86827c7b3104a76b1f4484a56bbef1ef08cfb"], + }, + "timestamp": { + "threshold": 1, + "keyids": ["3af6b427c05274532231760f39d81212fdf8ac1a9f8fddf12722623ccec02fec"], + }, + }, + }) + } + + #[test] + fn de_ser_root_metadata_without_keyid_hash_algorithms() { + let jsn = jsn_root_metadata_without_keyid_hash_algos(); + let decoded: RootMetadata = serde_json::from_value(jsn.clone()).unwrap(); + let encoded = serde_json::to_value(decoded).unwrap(); + + assert_eq!(jsn, encoded); + } + + #[test] + fn de_ser_root_metadata_wrong_key_id() { + let jsn = jsn_root_metadata_without_keyid_hash_algos(); + let mut jsn_str = str::from_utf8(&Json::canonicalize(&jsn).unwrap()) + .unwrap() + .to_owned(); + // Replace the key id to something else. + jsn_str = jsn_str.replace( + "12435b260b6172bd750aeb102f54a347c56b109e0524ab1f144593c07af66356", + "00435b260b6172bd750aeb102f54a347c56b109e0524ab1f144593c07af66356", + ); + let decoded: RootMetadata = serde_json::from_str(&jsn_str).unwrap(); + assert_eq!(3, decoded.keys.len()); + } + + #[test] + fn sign_and_verify_root_metadata() { + let jsn = jsn_root_metadata_without_keyid_hash_algos(); + let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let decoded: RootMetadata = serde_json::from_value(jsn).unwrap(); + + let signed: SignedMetadata = + SignedMetadata::new(&decoded, &root_key).unwrap(); + let raw_root = signed.to_raw().unwrap(); + + assert_matches!( + verify_signatures( + &MetadataPath::root(), + &raw_root, + 1, + &[root_key.public().clone()] + ), + Ok(_) + ); + } + + #[test] + fn verify_signed_serialized_root_metadata() { + let jsn = json!({ + "signatures": [{ + "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", + "sig": "1f944e022d0b30c5a9ddc9c210026f396e18a17cc9a4ee92c339a8ee63357608dba8121847a825c3a5c84c1081435436bd784c8086c3103cdd1489e79cff2802" + }], + "signed": jsn_root_metadata_without_keyid_hash_algos() + }); + let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let decoded: SignedMetadata = + serde_json::from_value(jsn).unwrap(); + let raw_root = decoded.to_raw().unwrap(); + + assert_matches!( + verify_signatures( + &MetadataPath::root(), + &raw_root, + 1, + &[root_key.public().clone()] + ), + Ok(_) + ); + } + + #[test] + fn verify_signed_serialized_root_metadata_with_duplicate_sig() { + let jsn = json!({ + "signatures": [{ + "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", + "sig": "1f944e022d0b30c5a9ddc9c210026f396e18a17cc9a4ee92c339a8ee63357608dba8121847a825c3a5c84c1081435436bd784c8086c3103cdd1489e79cff2802" + }, + { + "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", + "sig": "1f944e022d0b30c5a9ddc9c210026f396e18a17cc9a4ee92c339a8ee63357608dba8121847a825c3a5c84c1081435436bd784c8086c3103cdd1489e79cff2802" + }], + "signed": jsn_root_metadata_without_keyid_hash_algos() + }); + let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let decoded: SignedMetadata = + serde_json::from_value(jsn).unwrap(); + let raw_root = decoded.to_raw().unwrap(); + + assert_matches!( + verify_signatures(&MetadataPath::root(), &raw_root, 2, &[root_key.public().clone()]), + Err(Error::MetadataMissingSignatures { + role, + number_of_valid_signatures: 1, + threshold: 2, + }) + if role == MetadataPath::root() + ); + assert_matches!( + verify_signatures( + &MetadataPath::root(), + &raw_root, + 1, + &[root_key.public().clone()] + ), + Ok(_) + ); + } + + fn verify_signature_with_unknown_fields(mut metadata: serde_json::Value) + where + M: Metadata, + { + let key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let public_keys = vec![key.public().clone()]; + + let mut standard = SignedMetadataBuilder::::from_raw_metadata(metadata.clone()) + .unwrap() + .sign(&key) + .unwrap() + .build() + .to_raw() + .unwrap() + .parse_untrusted() + .unwrap(); + + metadata.as_object_mut().unwrap().insert( + "custom".into(), + json!({ + "metadata": ["please", "sign", "me"], + "this-too": 42, + }), + ); + let mut custom = SignedMetadataBuilder::::from_raw_metadata(metadata) + .unwrap() + .sign(&key) + .unwrap() + .build() + .to_raw() + .unwrap() + .parse_untrusted() + .unwrap(); + + // Ensure the signatures are valid as-is. + assert_matches!( + verify_signatures( + &M::ROLE.into(), + &standard.to_raw().unwrap(), + 1, + &public_keys + ), + Ok(_) + ); + assert_matches!( + verify_signatures( + &M::ROLE.into(), + &custom.to_raw().unwrap(), + 1, + std::iter::once(key.public()) + ), + Ok(_) + ); + + // But not if the metadata was signed with custom fields and they are now missing or + // unexpected new fields appear. + std::mem::swap(&mut standard.metadata, &mut custom.metadata); + assert_matches!( + verify_signatures( + &M::ROLE.into(), + &standard.to_raw().unwrap(), + 1, + std::iter::once(key.public()) + ), + Err(Error::MetadataMissingSignatures { role, number_of_valid_signatures: 0, threshold: 1 }) + if role == M::ROLE.into() + ); + assert_matches!( + verify_signatures( + &M::ROLE.into(), + &custom.to_raw().unwrap(), + 1, + std::iter::once(key.public()) + ), + Err(Error::MetadataMissingSignatures { role, number_of_valid_signatures: 0, threshold: 1 }) + if role == M::ROLE.into() + ); + } + + #[test] + fn unknown_fields_included_in_root_metadata_signature() { + verify_signature_with_unknown_fields::( + jsn_root_metadata_without_keyid_hash_algos(), + ); + } + + #[test] + fn unknown_fields_included_in_timestamp_metadata_signature() { + verify_signature_with_unknown_fields::(make_timestamp()); + } + + #[test] + fn unknown_fields_included_in_snapshot_metadata_signature() { + verify_signature_with_unknown_fields::(make_snapshot()); + } + + #[test] + fn unknown_fields_included_in_targets_metadata_signature() { + verify_signature_with_unknown_fields::(make_targets()); + } + + #[test] + fn serde_timestamp_metadata() { + let description = MetadataDescription::new( + 1, + Some(100), + hashmap! { HashAlgorithm::Sha256 => HashValue::new(vec![]) }, + ) + .unwrap(); + + let timestamp = TimestampMetadataBuilder::from_metadata_description(description) + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .build() + .unwrap(); + + let jsn = json!({ + "_type": "timestamp", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "", + }, + }, + } + }); + + let encoded = serde_json::to_value(×tamp).unwrap(); + assert_eq!(encoded, jsn); + let decoded: TimestampMetadata = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, timestamp); + } + + // Deserialize timestamp metadata with optional length and hashes + #[test] + fn serde_timestamp_metadata_without_length_and_hashes() { + let description = MetadataDescription::new(1, None, HashMap::new()).unwrap(); + + let timestamp = TimestampMetadataBuilder::from_metadata_description(description) + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .build() + .unwrap(); + + let jsn = json!({ + "_type": "timestamp", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1 + }, + } + }); + + let encoded = serde_json::to_value(×tamp).unwrap(); + assert_eq!(encoded, jsn); + let decoded: TimestampMetadata = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, timestamp); + } + + #[test] + fn serde_timestamp_metadata_missing_snapshot() { + let jsn = json!({ + "_type": "timestamp", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": {} + }); + + assert_matches!( + serde_json::from_value::(jsn), + Err(ref err) if err.to_string() == "missing field `snapshot.json`" + ); + } + + #[test] + fn serde_timestamp_metadata_extra_metadata() { + let jsn = json!({ + "_type": "timestamp", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "", + }, + }, + "targets.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "", + }, + }, + } + }); + + assert_matches!( + serde_json::from_value::(jsn), + Err(ref err) if err.to_string() == + "unknown field `targets.json`, expected `snapshot.json`" + ); + } + + #[test] + fn serde_snapshot_metadata() { + let snapshot = SnapshotMetadataBuilder::new() + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .insert_metadata_description( + MetadataPath::new("targets").unwrap(), + MetadataDescription::new( + 1, + Some(100), + hashmap! { HashAlgorithm::Sha256 => HashValue::new(vec![]) }, + ) + .unwrap(), + ) + .build() + .unwrap(); + + let jsn = json!({ + "_type": "snapshot", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "targets.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "", + }, + }, + }, + }); + + let encoded = serde_json::to_value(&snapshot).unwrap(); + assert_eq!(encoded, jsn); + let decoded: SnapshotMetadata = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, snapshot); + } + + // Deserialize snapshot metadata with optional length and hashes + #[test] + fn serde_snapshot_optional_length_and_hashes() { + let snapshot = SnapshotMetadataBuilder::new() + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .insert_metadata_description( + MetadataPath::new("targets").unwrap(), + MetadataDescription::new(1, None, HashMap::new()).unwrap(), + ) + .build() + .unwrap(); + + let jsn = json!({ + "_type": "snapshot", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "targets.json": { + "version": 1, + }, + }, + }); + + let encoded = serde_json::to_value(&snapshot).unwrap(); + assert_eq!(encoded, jsn); + let decoded: SnapshotMetadata = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, snapshot); + } + + #[test] + fn serde_targets_metadata() { + block_on(async { + let targets = TargetsMetadataBuilder::new() + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .insert_target_from_slice( + TargetPath::new("insert-target-from-slice").unwrap(), + &b"foo"[..], + &[HashAlgorithm::Sha256], + ) + .unwrap() + .insert_target_from_reader( + TargetPath::new("insert-target-from-reader").unwrap(), + &b"foo"[..], + &[HashAlgorithm::Sha256], + ) + .await + .unwrap() + .insert_target_description( + TargetPath::new("insert-target-description-from-slice-with-custom").unwrap(), + TargetDescription::from_slice_with_custom( + &b"foo"[..], + &[HashAlgorithm::Sha256], + HashMap::new(), + ) + .unwrap(), + ) + .insert_target_description( + TargetPath::new("insert-target-description-from-reader-with-custom").unwrap(), + TargetDescription::from_reader_with_custom( + &b"foo"[..], + &[HashAlgorithm::Sha256], + hashmap! { + "foo".into() => 1.into(), + "bar".into() => "baz".into(), + }, + ) + .await + .unwrap(), + ) + .build() + .unwrap(); + + let jsn = json!({ + "_type": "targets", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "targets": { + "insert-target-from-slice": { + "length": 3, + "hashes": { + "sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483\ + bfa0f98a5e886266e7ae", + }, + }, + "insert-target-description-from-slice-with-custom": { + "length": 3, + "hashes": { + "sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483\ + bfa0f98a5e886266e7ae", + }, + }, + "insert-target-from-reader": { + "length": 3, + "hashes": { + "sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483\ + bfa0f98a5e886266e7ae", + }, + }, + "insert-target-description-from-reader-with-custom": { + "length": 3, + "hashes": { + "sha256": "2c26b46b68ffc68ff99b453c1d30413413422d706483\ + bfa0f98a5e886266e7ae", + }, + "custom": { + "foo": 1, + "bar": "baz", + }, + }, + }, + }); + + let encoded = serde_json::to_value(&targets).unwrap(); + assert_eq!(encoded, jsn); + let decoded: TargetsMetadata = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, targets); + }) + } + + #[test] + fn serde_targets_with_delegations_metadata() { + let key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let delegations = Delegations::new( + hashmap! { key.public().key_id().clone() => key.public().clone() }, + vec![Delegation::new( + MetadataPath::new("foo/bar").unwrap(), + false, + 1, + hashset!(key.public().key_id().clone()), + hashset!(TargetPath::new("baz/quux").unwrap()), + ) + .unwrap()], + ) + .unwrap(); + + let targets = TargetsMetadataBuilder::new() + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .delegations(delegations) + .build() + .unwrap(); + + let jsn = json!({ + "_type": "targets", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "targets": {}, + "delegations": { + "keys": { + "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyid_hash_algorithms": ["sha256", "sha512"], + "keyval": { + "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58\ + ee422500d38caf461c65a3b6", + } + }, + }, + "roles": [ + { + "name": "foo/bar", + "terminating": false, + "threshold": 1, + "keyids": ["a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a"], + "paths": ["baz/quux"], + }, + ], + } + }); + + let encoded = serde_json::to_value(&targets).unwrap(); + assert_eq!(encoded, jsn); + let decoded: TargetsMetadata = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, targets); + } + + #[test] + fn serde_signed_metadata() { + let snapshot = SnapshotMetadataBuilder::new() + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .insert_metadata_description( + MetadataPath::new("targets").unwrap(), + MetadataDescription::new( + 1, + Some(100), + hashmap! { HashAlgorithm::Sha256 => HashValue::new(vec![]) }, + ) + .unwrap(), + ) + .build() + .unwrap(); + + let key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + + let signed = SignedMetadata::::new(&snapshot, &key).unwrap(); + + let jsn = json!({ + "signatures": [ + { + "keyid": "a9f3ebc9b138762563a9c27b6edd439959e559709babd123e8d449ba2c18c61a", + "sig": "a9b97b2439cd41e9a8c62e4d2f8f73b25a06095e0a994e8631a0\ + 88977271909af2cc829c68637af98b07ebffeea308cc1a1c83d18fa2\ + 9ec401493973b3dfa90e", + } + ], + "signed": { + "_type": "snapshot", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "targets.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "", + }, + }, + }, + }, + }); + + let encoded = serde_json::to_value(&signed).unwrap(); + assert_eq!(encoded, jsn, "{:#?} != {:#?}", encoded, jsn); + let decoded: SignedMetadata = + serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, signed); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // + // Here there be test cases about what metadata is allowed to be parsed wherein we do all sorts + // of naughty things and make sure the parsers puke appropriately. + // ______________ + // ,===:'., `-._ + // `:.`---.__ `-._ + // `:. `--. `. + // \. `. `. + // (,,(, \. `. ____,-`., + // (,' `/ \. ,--.___`.' + // , ,' ,--. `, \.;' ` + // `(o, / \ : \; + // |,,' / / // + // j;; / ,' ,-//. ,---. , + // \;' / ,' / _ \ / _ \ ,'/ + // \ `' / \ `' / \ `.' / + // `.___,' `.__,' `.__,' + // + /////////////////////////////////////////////////////////////////////////////////////////////// + + // TODO test for mismatched ed25519/rsa keys/schemes + + fn make_root() -> serde_json::Value { + let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let snapshot_key = Ed25519PrivateKey::from_pkcs8(ED25519_2_PK8).unwrap(); + let targets_key = Ed25519PrivateKey::from_pkcs8(ED25519_3_PK8).unwrap(); + let timestamp_key = Ed25519PrivateKey::from_pkcs8(ED25519_4_PK8).unwrap(); + + let root = RootMetadataBuilder::new() + .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + .root_key(root_key.public().clone()) + .snapshot_key(snapshot_key.public().clone()) + .targets_key(targets_key.public().clone()) + .timestamp_key(timestamp_key.public().clone()) + .build() + .unwrap(); + + serde_json::to_value(&root).unwrap() + } + + fn make_snapshot() -> serde_json::Value { + let snapshot = SnapshotMetadataBuilder::new() + .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + .build() + .unwrap(); + + serde_json::to_value(&snapshot).unwrap() + } + + fn make_timestamp() -> serde_json::Value { + let description = + MetadataDescription::from_slice(&[][..], 1, &[HashAlgorithm::Sha256]).unwrap(); + + let timestamp = TimestampMetadataBuilder::from_metadata_description(description) + .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .build() + .unwrap(); + + serde_json::to_value(×tamp).unwrap() + } + + fn make_targets() -> serde_json::Value { + let targets = TargetsMetadata::new( + 1, + Utc.ymd(2038, 1, 1).and_hms(0, 0, 0), + hashmap!(), + Delegations::default(), + Default::default(), + ) + .unwrap(); + + serde_json::to_value(&targets).unwrap() + } + + fn make_delegations() -> serde_json::Value { + let key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8) + .unwrap() + .public() + .clone(); + let delegations = Delegations::new( + hashmap! { key.key_id().clone() => key.clone() }, + vec![Delegation::new( + MetadataPath::new("foo").unwrap(), + false, + 1, + hashset!(key.key_id().clone()), + hashset!(TargetPath::new("bar").unwrap()), + ) + .unwrap()], + ) + .unwrap(); + + serde_json::to_value(&delegations).unwrap() + } + + fn make_delegation() -> serde_json::Value { + let key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8) + .unwrap() + .public() + .clone(); + let delegation = Delegation::new( + MetadataPath::new("foo").unwrap(), + false, + 1, + hashset!(key.key_id().clone()), + hashset!(TargetPath::new("bar").unwrap()), + ) + .unwrap(); + + serde_json::to_value(&delegation).unwrap() + } + + fn set_version(value: &mut serde_json::Value, version: i64) { + match value.as_object_mut() { + Some(obj) => { + let _ = obj.insert("version".into(), json!(version)); + } + None => panic!(), + } + } + + // Refuse to deserialize root metadata if the version is not > 0 + #[test] + fn deserialize_json_root_illegal_version() { + let mut root_json = make_root(); + set_version(&mut root_json, 0); + assert!(serde_json::from_value::(root_json.clone()).is_err()); + + let mut root_json = make_root(); + set_version(&mut root_json, -1); + assert!(serde_json::from_value::(root_json).is_err()); + } + + // Refuse to deserialize root metadata if it contains duplicate keys + #[test] + fn deserialize_json_root_duplicate_keys() { + let root_json = r#"{ + "_type": "root", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "consistent_snapshot": false, + "keys": { + "09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyval": { + "public": "1410ae3053aa70bbfa98428a879d64d3002a3578f7dfaaeb1cb0764e860f7e0b" + } + }, + "09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1": { + "keytype": "ed25519", + "scheme": "ed25519", + "keyval": { + "public": "166376c90a7f717d027056272f361c252fb050bed1a067ff2089a0302fbab73d" + } + } + }, + "roles": { + "root": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"] + }, + "snapshot": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"] + }, + "targets": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"] + }, + "timestamp": { + "threshold": 1, + "keyids": ["09557ed63f91b5b95917d46f66c63ea79bdaef1b008ba823808bca849f1d18a1"] + } + } + }"#; + match serde_json::from_str::(root_json) { + Err(ref err) if err.is_data() => { + assert!( + err.to_string().starts_with("Cannot have duplicate keys"), + "unexpected err: {:?}", + err + ); + } + result => panic!("unexpected result: {:?}", result), + } + } + + fn set_threshold(value: &mut serde_json::Value, threshold: i32) { + match value.as_object_mut() { + Some(obj) => { + let _ = obj.insert("threshold".into(), json!(threshold)); + } + None => panic!(), + } + } + + // Refuse to deserialize role definitions with illegal thresholds + #[test] + fn deserialize_json_role_definition_illegal_threshold() { + let role_def = RoleDefinition::new( + 1, + hashset![Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8) + .unwrap() + .public() + .key_id() + .clone()], + ) + .unwrap(); + + let mut jsn = serde_json::to_value(&role_def).unwrap(); + set_threshold(&mut jsn, 0); + assert!(serde_json::from_value::(jsn).is_err()); + + let mut jsn = serde_json::to_value(&role_def).unwrap(); + set_threshold(&mut jsn, -1); + assert!(serde_json::from_value::(jsn).is_err()); + + let role_def = RoleDefinition::new( + 2, + hashset![ + Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8) + .unwrap() + .public() + .key_id() + .clone(), + Ed25519PrivateKey::from_pkcs8(ED25519_2_PK8) + .unwrap() + .public() + .key_id() + .clone(), + ], + ) + .unwrap(); + + let mut jsn = serde_json::to_value(&role_def).unwrap(); + set_threshold(&mut jsn, 3); + assert!(serde_json::from_value::(jsn).is_err()); + } + + // Refuse to deserialize root metadata with wrong type field + #[test] + fn deserialize_json_root_bad_type() { + let mut root = make_root(); + let _ = root + .as_object_mut() + .unwrap() + .insert("_type".into(), json!("snapshot")); + assert!(serde_json::from_value::(root).is_err()); + } + + // Refuse to deserialize root metadata with unknown spec version + #[test] + fn deserialize_json_root_bad_spec_version() { + let mut root = make_root(); + let _ = root + .as_object_mut() + .unwrap() + .insert("spec_version".into(), json!("0")); + assert!(serde_json::from_value::(root).is_err()); + } + + // Refuse to deserialize role definitions with duplicated key ids + #[test] + fn deserialize_json_role_definition_duplicate_key_ids() { + let key_id = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8) + .unwrap() + .public() + .key_id() + .clone(); + let role_def = RoleDefinition::new(1, hashset![key_id.clone()]).unwrap(); + let mut jsn = serde_json::to_value(&role_def).unwrap(); + + match jsn.as_object_mut() { + Some(obj) => match obj.get_mut("keyids").unwrap().as_array_mut() { + Some(arr) => arr.push(json!(key_id)), + None => panic!(), + }, + None => panic!(), + } + + assert!(serde_json::from_value::(jsn).is_err()); + } + + // Refuse to deserialize snapshot metadata with illegal versions + #[test] + fn deserialize_json_snapshot_illegal_version() { + let mut snapshot = make_snapshot(); + set_version(&mut snapshot, 0); + assert!(serde_json::from_value::(snapshot).is_err()); + + let mut snapshot = make_snapshot(); + set_version(&mut snapshot, -1); + assert!(serde_json::from_value::(snapshot).is_err()); + } + + // Refuse to deserialize snapshot metadata with wrong type field + #[test] + fn deserialize_json_snapshot_bad_type() { + let mut snapshot = make_snapshot(); + let _ = snapshot + .as_object_mut() + .unwrap() + .insert("_type".into(), json!("root")); + assert!(serde_json::from_value::(snapshot).is_err()); + } + + // Refuse to deserialize snapshot metadata with unknown spec version + #[test] + fn deserialize_json_snapshot_spec_version() { + let mut snapshot = make_snapshot(); + let _ = snapshot + .as_object_mut() + .unwrap() + .insert("spec_version".into(), json!("0")); + assert!(serde_json::from_value::(snapshot).is_err()); + } + + // Refuse to deserialize snapshot metadata if it contains duplicate metadata + #[test] + fn deserialize_json_snapshot_duplicate_metadata() { + let snapshot_json = r#"{ + "_type": "snapshot", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "targets.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "" + } + }, + "targets.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "" + } + } + } + }"#; + match serde_json::from_str::(snapshot_json) { + Err(ref err) if err.is_data() => {} + result => panic!("unexpected result: {:?}", result), + } + } + + // Refuse to deserialize timestamp metadata with illegal versions + #[test] + fn deserialize_json_timestamp_illegal_version() { + let mut timestamp = make_timestamp(); + set_version(&mut timestamp, 0); + assert!(serde_json::from_value::(timestamp).is_err()); + + let mut timestamp = make_timestamp(); + set_version(&mut timestamp, -1); + assert!(serde_json::from_value::(timestamp).is_err()); + } + + // Refuse to deserialize timestamp metadata with wrong type field + #[test] + fn deserialize_json_timestamp_bad_type() { + let mut timestamp = make_timestamp(); + let _ = timestamp + .as_object_mut() + .unwrap() + .insert("_type".into(), json!("root")); + assert!(serde_json::from_value::(timestamp).is_err()); + } + + // Refuse to deserialize timestamp metadata with unknown spec version + #[test] + fn deserialize_json_timestamp_bad_spec_version() { + let mut timestamp = make_timestamp(); + let _ = timestamp + .as_object_mut() + .unwrap() + .insert("spec_version".into(), json!("0")); + assert!(serde_json::from_value::(timestamp).is_err()); + } + + // Refuse to deserialize timestamp metadata if it contains duplicate metadata + #[test] + fn deserialize_json_timestamp_duplicate_metadata() { + let timestamp_json = r#"{ + "_type": "timestamp", + "spec_version": "1.0.0", + "version": 1, + "expires": "2017-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "" + } + }, + "snapshot.json": { + "version": 1, + "length": 100, + "hashes": { + "sha256": "" + } + } + } + }"#; + match serde_json::from_str::(timestamp_json) { + Err(ref err) if err.is_data() => {} + result => panic!("unexpected result: {:?}", result), + } + } + + // Refuse to deserialize targets metadata with illegal versions + #[test] + fn deserialize_json_targets_illegal_version() { + let mut targets = make_targets(); + set_version(&mut targets, 0); + assert!(serde_json::from_value::(targets).is_err()); + + let mut targets = make_targets(); + set_version(&mut targets, -1); + assert!(serde_json::from_value::(targets).is_err()); + } + + // Accept targets metadata with a version that does not fit in a u32. + #[test] + fn deserialize_json_targets_u64_version() { + let big_version: u64 = u32::MAX as u64 + 1; + + let mut targets = make_targets(); + targets + .as_object_mut() + .unwrap() + .insert("version".into(), json!(big_version)); + + let decoded: TargetsMetadata = serde_json::from_value(targets.clone()).unwrap(); + assert_eq!(decoded.version(), big_version); + + // Round-tripping preserves the large version. + let reencoded = serde_json::to_value(&decoded).unwrap(); + assert_eq!(reencoded, targets); + } + + // Accept targets metadata with the maximum u64 version. + #[test] + fn deserialize_json_targets_u64_max_version() { + let mut targets = make_targets(); + targets + .as_object_mut() + .unwrap() + .insert("version".into(), json!(u64::MAX)); + + let decoded: TargetsMetadata = serde_json::from_value(targets).unwrap(); + assert_eq!(decoded.version(), u64::MAX); + } + + // Accept snapshot metadata whose meta descriptions reference targets + // versions that do not fit in a u32. + #[test] + fn deserialize_json_snapshot_meta_u64_version() { + let big_version: u64 = u32::MAX as u64 + 42; + + let snapshot_json = json!({ + "_type": "snapshot", + "spec_version": "1.0.0", + "version": 1u64, + "expires": "2038-01-01T00:00:00Z", + "meta": { + "targets.json": { + "version": big_version, + } + } + }); + + let decoded: SnapshotMetadata = serde_json::from_value(snapshot_json).unwrap(); + let description = decoded + .meta() + .get(&MetadataPath::targets()) + .expect("targets description"); + assert_eq!(description.version(), big_version); + } + + // Refuse to deserialize targets metadata with wrong type field + #[test] + fn deserialize_json_targets_bad_type() { + let mut targets = make_targets(); + let _ = targets + .as_object_mut() + .unwrap() + .insert("_type".into(), json!("root")); + assert!(serde_json::from_value::(targets).is_err()); + } + + // Refuse to deserialize targets metadata with unknown spec version + #[test] + fn deserialize_json_targets_bad_spec_version() { + let mut targets = make_targets(); + let _ = targets + .as_object_mut() + .unwrap() + .insert("spec_version".into(), json!("0")); + assert!(serde_json::from_value::(targets).is_err()); + } + + // Refuse to deserialize delegations with duplicated roles + #[test] + fn deserialize_json_delegations_duplicated_roles() { + let mut delegations = make_delegations(); + let dupe = delegations + .as_object() + .unwrap() + .get("roles") + .unwrap() + .as_array() + .unwrap()[0] + .clone(); + delegations + .as_object_mut() + .unwrap() + .get_mut("roles") + .unwrap() + .as_array_mut() + .unwrap() + .push(dupe); + assert!(serde_json::from_value::(delegations).is_err()); + } + + // Refuse to deserialize a delegation with insufficient threshold + #[test] + fn deserialize_json_delegation_bad_threshold() { + let mut delegation = make_delegation(); + set_threshold(&mut delegation, 0); + assert!(serde_json::from_value::(delegation).is_err()); + + let mut delegation = make_delegation(); + set_threshold(&mut delegation, 2); + assert!(serde_json::from_value::(delegation).is_err()); + } + + // Refuse to deserialize a delegation with duplicate key IDs + #[test] + fn deserialize_json_delegation_duplicate_key_ids() { + let mut delegation = make_delegation(); + let dupe = delegation + .as_object() + .unwrap() + .get("keyids") + .unwrap() + .as_array() + .unwrap()[0] + .clone(); + delegation + .as_object_mut() + .unwrap() + .get_mut("keyids") + .unwrap() + .as_array_mut() + .unwrap() + .push(dupe); + assert!(serde_json::from_value::(delegation).is_err()); + } + + // Refuse to deserialize a delegation with duplicate paths + #[test] + fn deserialize_json_delegation_duplicate_paths() { + let mut delegation = make_delegation(); + let dupe = delegation + .as_object() + .unwrap() + .get("paths") + .unwrap() + .as_array() + .unwrap()[0] + .clone(); + delegation + .as_object_mut() + .unwrap() + .get_mut("paths") + .unwrap() + .as_array_mut() + .unwrap() + .push(dupe); + assert!(serde_json::from_value::(delegation).is_err()); + } + + // Refuse to deserialize a Delegations struct with duplicate keys + #[test] + fn deserialize_json_delegations_duplicate_keys() { + let delegations_json = r#"{ + "keys": { + "qfrfBrkB4lBBSDEBlZgaTGS_SrE6UfmON9kP4i3dJFY=": { + "public_key": "MCwwBwYDK2VwBQADIQDrisJrXJ7wJ5474-giYqk7zhb-WO5CJQDTjK9GHGWjtg==", + "scheme": "ed25519", + "type": "ed25519" + }, + "qfrfBrkB4lBBSDEBlZgaTGS_SrE6UfmON9kP4i3dJFY=": { + "public_key": "MCwwBwYDK2VwBQADIQDrisJrXJ7wJ5474-giYqk7zhb-WO5CJQDTjK9GHGWjtg==", + "scheme": "ed25519", + "type": "ed25519" + } + }, + "roles": [ + { + "keyids": [ + "qfrfBrkB4lBBSDEBlZgaTGS_SrE6UfmON9kP4i3dJFY=" + ], + "paths": [ + "bar" + ], + "role": "foo", + "terminating": false, + "threshold": 1 + } + ] + }"#; + match serde_json::from_str::(delegations_json) { + Err(ref err) if err.is_data() => {} + result => panic!("unexpected result: {:?}", result), + } + } +} diff --git a/vendor/tuf/src/repo_builder.rs b/vendor/tuf/src/repo_builder.rs new file mode 100644 index 0000000000..fcaa540434 --- /dev/null +++ b/vendor/tuf/src/repo_builder.rs @@ -0,0 +1,2963 @@ +//! Repository Builder + +use { + crate::{ + crypto::{self, HashAlgorithm, PrivateKey, PublicKey}, + database::Database, + error::{Error, Result}, + interchange::DataInterchange, + metadata::{ + Delegation, DelegationsBuilder, Metadata, MetadataDescription, MetadataPath, + MetadataVersion, RawSignedMetadata, RawSignedMetadataSet, RawSignedMetadataSetBuilder, + RootMetadata, RootMetadataBuilder, SignedMetadataBuilder, SnapshotMetadata, + SnapshotMetadataBuilder, TargetDescription, TargetPath, TargetsMetadata, + TargetsMetadataBuilder, TimestampMetadata, TimestampMetadataBuilder, + }, + repository::RepositoryStorage, + verify::Verified, + }, + chrono::{DateTime, Duration, Utc}, + futures_io::{AsyncRead, AsyncSeek}, + futures_util::AsyncSeekExt as _, + std::{collections::HashMap, io::SeekFrom, marker::PhantomData}, +}; + +mod private { + use super::*; + + /// Implement the [sealed] pattern to make public traits that cannot be externally modified. + /// + /// [sealed]: https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed + pub trait Sealed {} + + impl Sealed for Root {} + impl Sealed for Targets {} + impl Sealed for Snapshot {} + impl Sealed for Timestamp {} + impl Sealed for Done {} +} + +/// Trait to track each of the [RepoBuilder] building states. +/// +/// This trait is [sealed] to make +/// sure external users cannot implement the `State` crate with unexpected states. +/// +/// [sealed]: https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed +pub trait State: private::Sealed {} + +/// State to stage a root metadata. +#[doc(hidden)] +pub struct Root { + builder: RootMetadataBuilder, +} + +impl State for Root {} + +/// State to stage a targets metadata. +#[doc(hidden)] +pub struct Targets { + staged_root: Option>, + targets: HashMap, + delegation_keys: Vec, + delegation_roles: Vec, + file_hash_algorithms: Vec, + inherit_from_trusted_targets: bool, +} + +impl Targets { + fn new(staged_root: Option>) -> Self { + Self { + staged_root, + targets: HashMap::new(), + delegation_keys: vec![], + delegation_roles: vec![], + file_hash_algorithms: vec![HashAlgorithm::Sha256], + inherit_from_trusted_targets: true, + } + } +} + +impl State for Targets {} + +/// State to stage a snapshot metadata. +#[doc(hidden)] +pub struct Snapshot { + staged_root: Option>, + staged_targets: Option>, + include_targets_length: bool, + targets_hash_algorithms: Vec, + inherit_from_trusted_snapshot: bool, +} + +impl State for Snapshot {} + +impl Snapshot { + fn new( + staged_root: Option>, + staged_targets: Option>, + ) -> Self { + Self { + staged_root, + staged_targets, + include_targets_length: false, + targets_hash_algorithms: vec![], + inherit_from_trusted_snapshot: true, + } + } + + fn targets_description(&self) -> Result> { + 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) + } + } +} + +/// State to stage a timestamp metadata. +pub struct Timestamp { + staged_root: Option>, + staged_targets: Option>, + staged_snapshot: Option>, + include_snapshot_length: bool, + snapshot_hash_algorithms: Vec, +} + +impl Timestamp { + fn new(state: Snapshot, staged_snapshot: Option>) -> 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> { + 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 State for Timestamp {} + +/// The final state for building repository metadata. +pub struct Done { + staged_root: Option>, + staged_targets: Option>, + staged_snapshot: Option>, + staged_timestamp: Option>, +} + +impl State for Done {} + +struct Staged { + metadata: M, + raw: RawSignedMetadata, +} + +struct RepoContext<'a, D, R> +where + D: DataInterchange + Sync, + R: RepositoryStorage, +{ + repo: R, + db: Option<&'a Database>, + current_time: DateTime, + 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, +} + +impl<'a, D, R> RepoContext<'a, D, R> +where + D: DataInterchange + Sync, + R: RepositoryStorage, +{ + fn root_keys_changed(&self, root: &Verified) -> bool { + let root_keys_count = root.root_keys().count(); + if root_keys_count != self.trusted_root_keys.len() { + return true; + } + + for key in &self.trusted_root_keys { + if root.root().key_ids().get(key.public().key_id()).is_none() { + return true; + } + } + + false + } + fn targets_keys_changed(&self, root: &Verified) -> bool { + for key in &self.trusted_targets_keys { + if root + .targets() + .key_ids() + .get(key.public().key_id()) + .is_none() + { + return true; + } + } + + false + } + + fn snapshot_keys_changed(&self, root: &Verified) -> bool { + for key in &self.trusted_snapshot_keys { + if root + .snapshot() + .key_ids() + .get(key.public().key_id()) + .is_none() + { + return true; + } + } + + false + } + + fn timestamp_keys_changed(&self, root: &Verified) -> bool { + for key in &self.trusted_timestamp_keys { + if root + .timestamp() + .key_ids() + .get(key.public().key_id()) + .is_none() + { + return true; + } + } + + false + } +} + +fn sign<'a, D, I, M>(meta: &M, keys: I) -> Result> +where + D: DataInterchange, + M: Metadata, + I: IntoIterator, +{ + // Sign the root. + let mut signed_builder = SignedMetadataBuilder::::from_metadata(meta)?; + for key in keys { + signed_builder = signed_builder.sign(*key)?; + } + + signed_builder.build().to_raw() +} + +/// This helper builder simplifies the process of creating new metadata. +pub struct RepoBuilder<'a, D, R, S = Root> +where + D: DataInterchange + Sync, + R: RepositoryStorage, + S: State, +{ + ctx: RepoContext<'a, D, R>, + state: S, +} + +impl<'a, D, R> RepoBuilder<'a, D, R, Root> +where + D: DataInterchange + Sync, + R: RepositoryStorage, +{ + /// Create a [RepoBuilder] for creating metadata for a new repository. + /// + /// # Examples + /// + /// ```rust + /// # use { + /// # futures_executor::block_on, + /// # tuf::{ + /// # interchange::Json, + /// # crypto::Ed25519PrivateKey, + /// # repo_builder::RepoBuilder, + /// # repository::EphemeralRepository, + /// # }, + /// # }; + /// # + /// # let key = Ed25519PrivateKey::from_pkcs8( + /// # include_bytes!("../tests/ed25519/ed25519-1.pk8.der") + /// # ).unwrap(); + /// # + /// # block_on(async { + /// let mut repo = EphemeralRepository::::new(); + /// let _metadata = RepoBuilder::create(&mut repo) + /// .trusted_root_keys(&[&key]) + /// .trusted_targets_keys(&[&key]) + /// .trusted_snapshot_keys(&[&key]) + /// .trusted_timestamp_keys(&[&key]) + /// .commit() + /// .await + /// .unwrap(); + /// # }); + /// ``` + pub fn create(repo: R) -> Self { + Self { + ctx: RepoContext { + repo, + db: None, + current_time: Utc::now(), + 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), + }, + } + } + + /// Create a [RepoBuilder] for creating metadata based off the latest metadata in the + /// [Database]. + /// + /// # Examples + /// + /// ```rust + /// # use { + /// # futures_executor::block_on, + /// # tuf::{ + /// # database::Database, + /// # crypto::Ed25519PrivateKey, + /// # interchange::Json, + /// # repo_builder::RepoBuilder, + /// # repository::EphemeralRepository, + /// # }, + /// # }; + /// # + /// # let key = Ed25519PrivateKey::from_pkcs8( + /// # include_bytes!("../tests/ed25519/ed25519-1.pk8.der") + /// # ).unwrap(); + /// # + /// # block_on(async { + /// let mut repo = EphemeralRepository::::new(); + /// let metadata1 = RepoBuilder::create(&mut repo) + /// .trusted_root_keys(&[&key]) + /// .trusted_targets_keys(&[&key]) + /// .trusted_snapshot_keys(&[&key]) + /// .trusted_timestamp_keys(&[&key]) + /// .commit() + /// .await + /// .unwrap(); + /// + /// let database = Database::from_trusted_metadata(&metadata1).unwrap(); + /// + /// let _metadata2 = RepoBuilder::from_database(&mut repo, &database) + /// .trusted_root_keys(&[&key]) + /// .trusted_targets_keys(&[&key]) + /// .trusted_snapshot_keys(&[&key]) + /// .trusted_timestamp_keys(&[&key]) + /// .stage_root() + /// .unwrap() + /// .commit() + /// .await + /// .unwrap(); + /// # }); + /// ``` + pub fn from_database(repo: R, db: &'a Database) -> 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), + current_time: Utc::now(), + 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 }, + } + } + + /// Change the time the builder will use to see if metadata is expired, and the base time to use + /// to compute the next expiration. + /// + /// Default is the current wall clock time in UTC. + pub fn current_time(mut self, current_time: DateTime) -> Self { + self.ctx.current_time = current_time; + self + } + + /// Sign the root metadata with `keys`, but do not include the keys as trusted root keys in the + /// root metadata. This is typically used to support root key rotation. + pub fn signing_root_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self { + for key in keys { + self.ctx.signing_root_keys.push(*key); + } + self + } + + /// Sign the targets metadata with `keys`, but do not include the keys as trusted targets keys + /// in the root metadata. This is typically used to support targets key rotation. + pub fn signing_targets_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self { + for key in keys { + self.ctx.signing_targets_keys.push(*key); + } + self + } + + /// Sign the snapshot metadata with `keys`, but do not include the keys as trusted snapshot keys + /// in the root metadata. This is typically used to support snapshot key rotation. + pub fn signing_snapshot_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self { + for key in keys { + self.ctx.signing_snapshot_keys.push(*key); + } + self + } + + /// Sign the timestamp metadata with `keys`, but do not include the keys as trusted timestamp + /// keys in the root metadata. This is typically used to support timestamp key rotation. + pub fn signing_timestamp_keys(mut self, keys: &[&'a dyn PrivateKey]) -> Self { + for key in keys { + self.ctx.signing_timestamp_keys.push(*key); + } + self + } + + /// Sign the root metadata with `keys`, and include the keys as trusted root keys in the root + /// metadata. + 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 + } + + /// Sign the targets metadata with `keys`, and include the keys as trusted targets keys in the + /// targets metadata. + 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 + } + + /// Sign the snapshot metadata with `keys`, and include the keys as trusted snapshot keys in the + /// root metadata. + 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 + } + + /// Sign the timestamp metadata with `keys`, and include the keys as + /// trusted timestamp keys in the root metadata. + 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 + } + + /// Stage a root metadata. + /// + /// If this is a new repository, the root will be staged with: + /// + /// * version: 1 + /// * consistent_snapshot: true + /// * expires: 365 days from the current day. + /// * root_threshold: 1 + /// * targets_threshold: 1 + /// * snapshot_threshold: 1 + /// * timestamp_threshold: 1 + /// + /// Otherwise, it will be staged with: + /// + /// * version: 1 after the trusted root's version + /// * expires: 365 days from the current day. + /// * consistent_snapshot: match the trusted root's consistent snapshot + /// * root_threshold: match the trusted root's root threshold + /// * targets_threshold: match the trusted root's targets threshold + /// * snapshot_threshold: match the trusted root's snapshot threshold + /// * timestamp_threshold: match the trusted root's timestamp threshold + pub fn stage_root(self) -> Result>> { + self.stage_root_with_builder(|builder| builder) + } + + /// Stage a new root using the default settings if: + /// + /// * There is no trusted root metadata. + /// * The trusted keys are different from the keys that are in the trusted root. + /// * The trusted root metadata has expired. + pub fn stage_root_if_necessary(self) -> Result>> { + if self.need_new_root() { + self.stage_root() + } else { + Ok(self.skip_root()) + } + } + + /// Skip creating the root metadata. This may cause [commit](#method.commit-4) to fail if this + /// is a new repository. + pub fn skip_root(self) -> RepoBuilder<'a, D, R, Targets> { + RepoBuilder { + ctx: self.ctx, + state: Targets::new(None), + } + } + + /// Initialize a [RootMetadataBuilder] and pass it to the closure for further configuration. + /// This builder will then be used to generate and stage a new [RootMetadata] for eventual + /// commitment to the repository. + /// + /// If this is a new repository, the builder will be initialized with the following defaults: + /// + /// * version: 1 + /// * consistent_snapshot: true + /// * expires: 365 days from the current day. + /// * root_threshold: 1 + /// * targets_threshold: 1 + /// * snapshot_threshold: 1 + /// * timestamp_threshold: 1 + /// + /// Otherwise, it will be initialized with: + /// + /// * version: 1 after the trusted root's version + /// * expires: 365 days from the current day. + /// * consistent_snapshot: match the trusted root's consistent snapshot + /// * root_threshold: match the trusted root's root threshold + /// * targets_threshold: match the trusted root's targets threshold + /// * snapshot_threshold: match the trusted root's snapshot threshold + /// * timestamp_threshold: match the trusted root's timestamp threshold + pub fn stage_root_with_builder(self, f: F) -> Result>> + 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::MetadataVersionMustBeSmallerThanMaxU64(MetadataPath::root()) + })? + } else { + 1 + }; + + let root_builder = self + .state + .builder + .version(next_version) + .expires(self.ctx.current_time + 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, + })), + }) + } + + /// Add a target that's loaded in from the reader. This will store the target in the repository, + /// and may stage a root metadata if necessary. + /// + /// This will hash the file with [HashAlgorithm::Sha256]. + /// + /// See [RepoBuilder::add_target] for more details. + pub async fn add_target( + self, + target_path: TargetPath, + reader: Rd, + ) -> Result>> + where + Rd: AsyncRead + AsyncSeek + Unpin + Send, + { + self.stage_root_if_necessary()? + .add_target(target_path, reader) + .await + } + + /// Validate and write the metadata to the repository. + /// + /// This may stage a root, targets, snapshot, and timestamp metadata if necessary. + /// + /// See [RepoBuilder::commit] for more details. + pub async fn commit(self) -> Result> { + self.stage_root_if_necessary()?.commit().await + } + + /// Check if we need a new root database. + fn need_new_root(&self) -> bool { + // We need a new root metadata if we don't have a database yet. + let trusted_root = if let Some(db) = self.ctx.db { + db.trusted_root() + } else { + return true; + }; + + // We need a new root metadata if the metadata expired. + if trusted_root.expires() <= &self.ctx.current_time { + return true; + } + + // Sign the metadata if we passed in any old root keys. + if !self.ctx.signing_root_keys.is_empty() { + return true; + } + + // Otherwise, see if any of the keys have changed. + self.ctx.root_keys_changed(trusted_root) + || self.ctx.targets_keys_changed(trusted_root) + || self.ctx.snapshot_keys_changed(trusted_root) + || self.ctx.timestamp_keys_changed(trusted_root) + } +} + +impl<'a, D, R> RepoBuilder<'a, D, R, Targets> +where + D: DataInterchange + Sync, + R: RepositoryStorage, +{ + /// Whether or not to include the length of the targets, and any delegated targets, in the + /// new snapshot. + /// + /// Default is `[HashAlgorithm::Sha256]`. + pub fn target_hash_algorithms(mut self, algorithms: &[HashAlgorithm]) -> Self { + self.state.file_hash_algorithms = algorithms.to_vec(); + self + } + + /// Whether or not the new targets metadata inherits targets and delegations from the trusted + /// targets metadata. + /// + /// Default is `true`. + pub fn inherit_from_trusted_targets(mut self, inherit: bool) -> Self { + self.state.inherit_from_trusted_targets = inherit; + self + } + + /// Stage a targets metadata using the default settings. + pub fn stage_targets(self) -> Result>> { + self.stage_targets_with_builder(|builder| builder) + } + + /// Stage a new targets using the default settings if: + /// + /// * There is no trusted targets metadata. + /// * The trusted targets metadata has expired. + pub fn stage_targets_if_necessary(self) -> Result>> { + if self.need_new_targets() { + self.stage_targets_with_builder(|builder| builder) + } else { + Ok(self.skip_targets()) + } + } + + /// Skip creating the targets metadata. + pub fn skip_targets(self) -> RepoBuilder<'a, D, R, Snapshot> { + RepoBuilder { + ctx: self.ctx, + state: Snapshot::new(self.state.staged_root, None), + } + } + + /// Add a target that's loaded in from the reader. This will store the target in the repository. + /// + /// This will hash the file with the hash specified in [RepoBuilder::target_hash_algorithms]. If + /// none was specified, the file will be hashed with [HashAlgorithm::Sha256]. + pub async fn add_target( + self, + target_path: TargetPath, + reader: Rd, + ) -> Result>> + where + Rd: AsyncRead + AsyncSeek + Unpin + Send, + { + self.add_target_with_custom(target_path, reader, HashMap::new()) + .await + } + + /// Add a target that's loaded in from the reader. This will store the target in the repository. + /// + /// This will hash the file with the hash specified in [RepoBuilder::target_hash_algorithms]. If + /// none was specified, the file will be hashed with [HashAlgorithm::Sha256]. + pub async fn add_target_with_custom( + mut self, + target_path: TargetPath, + mut reader: Rd, + custom: HashMap, + ) -> Result>> + 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::MetadataNotFound { + path: MetadataPath::root(), + version: MetadataVersion::None, + }); + }; + + let target_description = TargetDescription::from_reader_with_custom( + &mut reader, + &self.state.file_hash_algorithms, + custom, + ) + .await?; + + // According to TUF section 5.5.2, when consistent snapshot is enabled, target files should be + // stored at `$HASH.FILENAME.EXT`. Otherwise it is stored at `FILENAME.EXT`. + 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.targets.insert(target_path, target_description); + + Ok(self) + } + + /// Add a target delegation key. + pub fn add_delegation_key(mut self, key: PublicKey) -> Self { + self.state.delegation_keys.push(key); + self + } + + /// Add a target delegation role. + pub fn add_delegation_role(mut self, delegation: Delegation) -> Self { + self.state.delegation_roles.push(delegation); + self + } + + /// Initialize a [TargetsMetadataBuilder] and pass it to the closure for further configuration. + /// This builder will then be used to generate and stage a new [TargetsMetadata] for eventual + /// commitment to the repository. + /// + /// This builder will be initialized with: + /// + /// * version: 1 if a new repository, otherwise 1 past the trusted targets's version. + /// * expires: 90 days from the current day. + pub fn stage_targets_with_builder(self, f: F) -> Result>> + where + F: FnOnce(TargetsMetadataBuilder) -> TargetsMetadataBuilder, + { + let mut targets_builder = TargetsMetadataBuilder::new(); + let mut delegations_builder = DelegationsBuilder::new(); + + if let Some(trusted_targets) = self.ctx.db.and_then(|db| db.trusted_targets()) { + let next_version = trusted_targets.version().checked_add(1).ok_or_else(|| { + Error::MetadataVersionMustBeSmallerThanMaxU64(MetadataPath::targets()) + })?; + + targets_builder = targets_builder.version(next_version); + + // Insert all the metadata from the trusted snapshot. + if self.state.inherit_from_trusted_targets { + for (target_path, target_description) in trusted_targets.targets() { + targets_builder = targets_builder + .insert_target_description(target_path.clone(), target_description.clone()); + } + + for key in trusted_targets.delegations().keys().values() { + delegations_builder = delegations_builder.key(key.clone()); + } + + for role in trusted_targets.delegations().roles() { + delegations_builder = delegations_builder.role(role.clone()); + } + } + } + + // Overwrite any of the old targets with the new ones. + for (target_path, target_description) in self.state.targets { + targets_builder = targets_builder + .insert_target_description(target_path.clone(), target_description.clone()); + } + + // Overwrite the old delegation keys. + for key in self.state.delegation_keys { + delegations_builder = delegations_builder.key(key); + } + + // Overwrite the old delegation roles. + for role in self.state.delegation_roles { + delegations_builder = delegations_builder.role(role); + } + + targets_builder = targets_builder.delegations(delegations_builder.build()?); + + let targets = f(targets_builder).build()?; + + // Sign the targets metadata. + 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, + }), + ), + }) + } + + /// Validate and write the metadata to the repository. + /// + /// This may stage a targets, snapshot, and timestamp metadata if necessary. + /// + /// See [RepoBuilder::commit](#method.commit-4) for more details. + pub async fn commit(self) -> Result> { + self.stage_targets_if_necessary()?.commit().await + } + + /// Commit the metadata for this repository without validating it. + /// + /// Warning: This can write invalid metadata to a repository without + /// validating that it is correct. + #[cfg(test)] + pub async fn commit_skip_validation(self) -> Result> { + self.stage_targets_if_necessary()? + .commit_skip_validation() + .await + } + + fn need_new_targets(&self) -> bool { + // We need a new targets metadata if we added any targets. + if !self.state.targets.is_empty() { + return true; + } + + // We need a new targets metadata if we staged a new root. + if self.state.staged_root.is_some() { + return true; + } + + // We need a new targets metadata if we don't have a database yet. + let db = if let Some(ref db) = self.ctx.db { + db + } else { + return true; + }; + + // We need a new targets metadata if the database doesn't have a targets. + let trusted_targets = if let Some(trusted_targets) = db.trusted_targets() { + trusted_targets + } else { + return true; + }; + + // We need a new targets metadata if the metadata expired. + if trusted_targets.expires() <= &self.ctx.current_time { + return true; + } + + // Otherwise, see if the targets keys have changed. + self.ctx.targets_keys_changed(db.trusted_root()) + } +} + +impl<'a, D, R> RepoBuilder<'a, D, R, Snapshot> +where + D: DataInterchange + Sync, + R: RepositoryStorage, +{ + /// Whether or not to include the length of the targets, and any delegated targets, in the + /// new snapshot. + /// + /// Default is `false`. + pub fn snapshot_includes_length(mut self, include_targets_lengths: bool) -> Self { + self.state.include_targets_length = include_targets_lengths; + self + } + + /// Whether or not to include the hashes of the targets, and any delegated targets, in the + /// new snapshot. + /// + /// Default is `&[]`. + pub fn snapshot_includes_hashes(mut self, hashes: &[HashAlgorithm]) -> Self { + self.state.targets_hash_algorithms = hashes.to_vec(); + self + } + + /// Whether or not the new snapshot to inherit metafiles from the trusted snapshot. + /// + /// Default is `true`. + pub fn inherit_from_trusted_snapshot(mut self, inherit: bool) -> Self { + self.state.inherit_from_trusted_snapshot = inherit; + self + } + + /// Stage a snapshot metadata using the default settings. + pub fn stage_snapshot(self) -> Result>> { + self.stage_snapshot_with_builder(|builder| builder) + } + + /// Stage a new snapshot using the default settings if: + /// + /// * There is no trusted snapshot metadata. + /// * The trusted snapshot metadata has expired. + pub fn stage_snapshot_if_necessary(self) -> Result>> { + if self.need_new_snapshot() { + self.stage_snapshot() + } else { + Ok(self.skip_snapshot()) + } + } + + /// Skip creating the snapshot metadata. + pub fn skip_snapshot(self) -> RepoBuilder<'a, D, R, Timestamp> { + RepoBuilder { + ctx: self.ctx, + state: Timestamp::new(self.state, None), + } + } + + /// Initialize a [SnapshotMetadataBuilder] and pass it to the closure for further configuration. + /// This builder will then be used to generate and stage a new [SnapshotMetadata] for eventual + /// commitment to the repository. + /// + /// This builder will be initialized with: + /// + /// * version: 1 if a new repository, otherwise 1 past the trusted snapshot's version. + /// * expires: 7 days from the current day. + pub fn stage_snapshot_with_builder(self, f: F) -> Result>> + where + F: FnOnce(SnapshotMetadataBuilder) -> SnapshotMetadataBuilder, + { + let mut snapshot_builder = + SnapshotMetadataBuilder::new().expires(self.ctx.current_time + Duration::days(7)); + + if let Some(trusted_snapshot) = self.ctx.db.and_then(|db| db.trusted_snapshot()) { + let next_version = trusted_snapshot.version().checked_add(1).ok_or_else(|| { + Error::MetadataVersionMustBeSmallerThanMaxU64(MetadataPath::snapshot()) + })?; + + snapshot_builder = snapshot_builder.version(next_version); + + // Insert all the metadata from the trusted snapshot. + if self.state.inherit_from_trusted_snapshot { + for (path, description) in trusted_snapshot.meta() { + snapshot_builder = snapshot_builder + .insert_metadata_description(path.clone(), description.clone()); + } + } + } + + // Overwrite the targets entry if specified. + if let Some(targets_description) = self.state.targets_description()? { + snapshot_builder = snapshot_builder + .insert_metadata_description(MetadataPath::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, + }), + ), + }) + } + + /// Validate and write the metadata to the repository. + /// + /// This may stage a snapshot and timestamp metadata if necessary. + /// + /// See [RepoBuilder::commit](#method.commit-4) for more details. + pub async fn commit(self) -> Result> { + self.stage_snapshot_if_necessary()?.commit().await + } + + /// Commit the metadata for this repository without validating it. + /// + /// Warning: This can write invalid metadata to a repository without + /// validating that it is correct. + #[cfg(test)] + pub async fn commit_skip_validation(self) -> Result> { + self.stage_snapshot_if_necessary()? + .commit_skip_validation() + .await + } + + fn need_new_snapshot(&self) -> bool { + // We need a new snapshot metadata if we staged a new root. + if self.state.staged_root.is_some() { + return true; + } + + // We need a new snapshot metadata if we staged a new targets. + if self.state.staged_targets.is_some() { + return true; + } + + // We need a new snapshot metadata if we don't have a database yet. + let db = if let Some(ref db) = self.ctx.db { + db + } else { + return true; + }; + + // We need a new snapshot metadata if the database doesn't have a snapshot. + let trusted_snapshot = if let Some(trusted_snapshot) = db.trusted_snapshot() { + trusted_snapshot + } else { + return true; + }; + + // We need a new snapshot metadata if the metadata expired. + if trusted_snapshot.expires() <= &self.ctx.current_time { + return true; + } + + // Otherwise, see if the snapshot keys have changed. + self.ctx.snapshot_keys_changed(db.trusted_root()) + } +} + +impl<'a, D, R> RepoBuilder<'a, D, R, Timestamp> +where + D: DataInterchange + Sync, + R: RepositoryStorage, +{ + /// Whether or not to include the length of the snapshot, and any delegated snapshot, in the + /// new snapshot. + pub fn timestamp_includes_length(mut self, include_snapshot_lengths: bool) -> Self { + self.state.include_snapshot_length = include_snapshot_lengths; + self + } + + /// Whether or not to include the hashes of the snapshot in the + /// new timestamp. + pub fn timestamp_includes_hashes(mut self, hashes: &[HashAlgorithm]) -> Self { + self.state.snapshot_hash_algorithms = hashes.to_vec(); + self + } + + /// Stage a timestamp metadata using the default settings. + /// + /// Note: This will also: + /// * stage a root metadata with the default settings if necessary. + /// * stage a targets metadata if necessary. + /// * stage a snapshot metadata if necessary. + pub fn stage_timestamp(self) -> Result>> { + self.stage_timestamp_with_builder(|builder| builder) + } + + /// Stage a new timestamp using the default settings if: + /// + /// * There is no trusted timestamp metadata. + /// * The trusted timestamp metadata has expired. + pub fn stage_timestamp_if_necessary(self) -> Result>> { + if self.need_new_timestamp() { + self.stage_timestamp() + } else { + Ok(self.skip_timestamp()) + } + } + + /// Skip creating the timestamp metadata. + pub fn skip_timestamp(self) -> RepoBuilder<'a, D, R, Done> { + 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, + }, + } + } + + /// Initialize a [TimestampMetadataBuilder] and pass it to the closure for further configuration. + /// This builder will then be used to generate and stage a new [TimestampMetadata] for eventual + /// commitment to the repository. + /// + /// This builder will be initialized with: + /// + /// * version: 1 if a new repository, otherwise 1 past the trusted snapshot's version. + /// * expires: 1 day from the current day. + pub fn stage_timestamp_with_builder(self, f: F) -> Result>> + 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::MetadataVersionMustBeSmallerThanMaxU64(MetadataPath::timestamp()) + })? + } 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_else(|| Error::MetadataNotFound { + path: MetadataPath::snapshot(), + version: MetadataVersion::None, + })? + }; + + let timestamp_builder = TimestampMetadataBuilder::from_metadata_description(description) + .version(next_version) + .expires(self.ctx.current_time + 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, + }), + }, + }) + } + + /// See [RepoBuilder::commit](#method.commit-4) for more details. + pub async fn commit(self) -> Result> { + self.stage_timestamp_if_necessary()?.commit().await + } + + /// Commit the metadata for this repository without validating it. + /// + /// Warning: This can write invalid metadata to a repository without + /// validating that it is correct. + #[cfg(test)] + pub async fn commit_skip_validation(self) -> Result> { + self.stage_timestamp_if_necessary()? + .commit_skip_validation() + .await + } + + fn need_new_timestamp(&self) -> bool { + // We need a new timestamp metadata if we staged a new root. + if self.state.staged_root.is_some() { + return true; + } + + // We need a new timestamp metadata if we staged a new snapshot. + if self.state.staged_snapshot.is_some() { + return true; + } + + // We need a new timestamp metadata if we don't have a database yet. + let db = if let Some(ref db) = self.ctx.db { + db + } else { + return true; + }; + + // We need a new timestamp metadata if the database doesn't have a timestamp. + let trusted_timestamp = if let Some(trusted_timestamp) = db.trusted_timestamp() { + trusted_timestamp + } else { + return true; + }; + + // We need a new timestamp metadata if the metadata expired. + if trusted_timestamp.expires() <= &self.ctx.current_time { + return true; + } + + // Otherwise, see if the timestamp keys have changed. + self.ctx.timestamp_keys_changed(db.trusted_root()) + } +} + +impl<'a, D, R> RepoBuilder<'a, D, R, Done> +where + D: DataInterchange + Sync, + R: RepositoryStorage, +{ + /// Commit the metadata for this repository, then write all metadata to the repository. Before + /// writing the metadata to `repo`, this will test that a client can update to this metadata to + /// make sure it is valid. + pub async fn commit(mut self) -> Result> { + self.validate_built_metadata()?; + self.write_repo().await?; + Ok(self.build_skip_validation()) + } + + /// Commit the metadata for this repository without validating it. + /// + /// Warning: This can write invalid metadata to a repository without validating that it is + /// correct. + #[cfg(test)] + pub async fn commit_skip_validation(mut self) -> Result> { + self.write_repo().await?; + Ok(self.build_skip_validation()) + } + + /// Build the metadata without validating it for correctness. + /// + /// Warning: This can produce invalid metadata. + fn build_skip_validation(self) -> RawSignedMetadataSet { + 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() + } + + /// Before we commit any metadata, make sure that we can update from our + /// current TUF database to the latest version. + fn validate_built_metadata(&self) -> Result<()> { + // Use a TUF database to make sure we can update to the metadata we just + // produced. If we were constructed with a database, create a copy of it + // and make sure we can install the update. + 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::MetadataNotFound { + path: MetadataPath::root(), + version: MetadataVersion::None, + }); + }; + + if let Some(ref timestamp) = self.state.staged_timestamp { + db.update_timestamp(&self.ctx.current_time, ×tamp.raw)?; + } + + if let Some(ref snapshot) = self.state.staged_snapshot { + db.update_snapshot(&self.ctx.current_time, &snapshot.raw)?; + } + + if let Some(ref targets) = self.state.staged_targets { + db.update_targets(&self.ctx.current_time, &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::root(), + MetadataVersion::Number(root.metadata.version()), + &mut root.raw.as_bytes(), + ) + .await?; + + self.ctx + .repo + .store_metadata( + &MetadataPath::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::MetadataNotFound { + path: MetadataPath::root(), + version: MetadataVersion::None, + }); + }; + + if let Some(ref targets) = self.state.staged_targets { + let path = MetadataPath::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::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::timestamp(), + MetadataVersion::None, + &mut timestamp.raw.as_bytes(), + ) + .await?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use chrono::NaiveDateTime; + + use crate::repository::RepositoryProvider; + + use { + super::*, + crate::{ + client::{Client, Config}, + crypto::Ed25519PrivateKey, + interchange::Json, + metadata::SignedMetadata, + repository::EphemeralRepository, + }, + assert_matches::assert_matches, + chrono::{ + offset::{TimeZone as _, Utc}, + DateTime, + }, + futures_executor::block_on, + futures_util::io::{AsyncReadExt, Cursor}, + lazy_static::lazy_static, + maplit::hashmap, + pretty_assertions::assert_eq, + std::collections::BTreeMap, + }; + + lazy_static! { + static ref KEYS: Vec = { + 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: u64, + consistent_snapshot: bool, + expires: DateTime, + ) -> SignedMetadata { + 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: u64, + expires: DateTime, + ) -> SignedMetadata { + let targets = TargetsMetadataBuilder::new() + .version(version) + .expires(expires) + .build() + .unwrap(); + SignedMetadataBuilder::::from_metadata(&targets) + .unwrap() + .sign(&KEYS[1]) + .unwrap() + .sign(&KEYS[2]) + .unwrap() + .sign(&KEYS[3]) + .unwrap() + .build() + } + + fn create_snapshot( + version: u64, + expires: DateTime, + targets: &SignedMetadata, + include_length_and_hashes: bool, + ) -> SignedMetadata { + 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::targets(), description) + .version(version) + .expires(expires) + .build() + .unwrap(); + SignedMetadataBuilder::::from_metadata(&snapshot) + .unwrap() + .sign(&KEYS[2]) + .unwrap() + .sign(&KEYS[3]) + .unwrap() + .sign(&KEYS[4]) + .unwrap() + .build() + } + + fn create_timestamp( + version: u64, + expires: DateTime, + snapshot: &SignedMetadata, + include_length_and_hashes: bool, + ) -> SignedMetadata { + 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::::from_metadata(×tamp) + .unwrap() + .sign(&KEYS[3]) + .unwrap() + .sign(&KEYS[4]) + .unwrap() + .sign(&KEYS[5]) + .unwrap() + .build() + } + + fn assert_metadata( + metadata: &RawSignedMetadataSet, + expected_root: Option<&RawSignedMetadata>, + expected_targets: Option<&RawSignedMetadata>, + expected_snapshot: Option<&RawSignedMetadata>, + expected_timestamp: Option<&RawSignedMetadata>, + ) { + 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, + 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::>(); + + let expected_metadata = expected_metadata + .iter() + .map(|(k, v)| (k.clone(), String::from_utf8_lossy(v).to_string())) + .collect::>(); + + assert_eq!( + actual_metadata.keys().collect::>(), + expected_metadata.keys().collect::>() + ); + 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) { + // We'll write all the metadata to this remote repository. + let mut remote = EphemeralRepository::::new(); + + // First, create the metadata. + 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]) + .stage_timestamp_with_builder(|builder| builder.expires(expires1)) + .unwrap() + .commit() + .await + .unwrap(); + + // Generate the expected metadata by hand, and make sure we produced + // what we expected. + 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), + ); + + // Make sure we stored the metadata correctly. + let mut expected_metadata: BTreeMap<_, _> = vec![ + ( + (MetadataPath::root(), MetadataVersion::Number(1)), + raw_root1.as_bytes(), + ), + ( + (MetadataPath::root(), MetadataVersion::None), + raw_root1.as_bytes(), + ), + ( + (MetadataPath::targets(), MetadataVersion::None), + raw_targets1.as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::None), + raw_snapshot1.as_bytes(), + ), + ( + (MetadataPath::timestamp(), MetadataVersion::None), + raw_timestamp1.as_bytes(), + ), + ] + .into_iter() + .collect(); + + if consistent_snapshot { + expected_metadata.extend(vec![ + ( + (MetadataPath::targets(), MetadataVersion::Number(1)), + raw_targets1.as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::Number(1)), + raw_snapshot1.as_bytes(), + ), + ]); + } + + assert_repo(&remote, &expected_metadata); + + // Create a client, and make sure we can update to the version we + // just made. + let mut client = Client::with_trusted_root( + Config::default(), + metadata1.root().unwrap(), + EphemeralRepository::new(), + remote, + ) + .await + .unwrap(); + client.update().await.unwrap(); + assert_eq!(client.database().trusted_root().version(), 1); + assert_eq!( + client.database().trusted_targets().map(|m| m.version()), + Some(1) + ); + assert_eq!( + client.database().trusted_snapshot().map(|m| m.version()), + Some(1) + ); + assert_eq!( + client.database().trusted_timestamp().map(|m| m.version()), + Some(1) + ); + + // Create a new metadata, derived from the tuf database we created + // with the client. + 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(&[]) + .stage_timestamp_with_builder(|builder| builder.expires(expires2)) + .unwrap() + .commit() + .await + .unwrap(); + + // Make sure the new metadata was generated as expected. + 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), + ); + + // Check that the new metadata was written. + expected_metadata.extend(vec![ + ( + (MetadataPath::root(), MetadataVersion::Number(2)), + raw_root2.as_bytes(), + ), + ( + (MetadataPath::root(), MetadataVersion::None), + raw_root2.as_bytes(), + ), + ( + (MetadataPath::targets(), MetadataVersion::None), + raw_targets2.as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::None), + raw_snapshot2.as_bytes(), + ), + ( + (MetadataPath::timestamp(), MetadataVersion::None), + raw_timestamp2.as_bytes(), + ), + ]); + + if consistent_snapshot { + expected_metadata.extend(vec![ + ( + (MetadataPath::targets(), MetadataVersion::Number(2)), + raw_targets2.as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::Number(2)), + raw_snapshot2.as_bytes(), + ), + ]); + } + + assert_repo(&parts.remote, &expected_metadata); + + // And make sure the client can update to the latest metadata. + let mut client = Client::from_parts(parts); + client.update().await.unwrap(); + assert_eq!(client.database().trusted_root().version(), 2); + assert_eq!( + client.database().trusted_targets().map(|m| m.version()), + Some(2) + ); + assert_eq!( + client.database().trusted_snapshot().map(|m| m.version()), + Some(2) + ); + assert_eq!( + client.database().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::::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.database().trusted_root().version(), 1); + + // Make sure doing another commit makes no changes. + 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.database().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::::new(); + + // First, create the initial metadata. We initially sign the root + // metadata with key 1. + 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.database().trusted_root().version(), 1); + assert_eq!( + client + .database() + .trusted_root() + .root_keys() + .collect::>(), + vec![KEYS[1].public()], + ); + + // Another update should not fetch anything. + assert!(!client.update().await.unwrap()); + assert_eq!(client.database().trusted_root().version(), 1); + + // Now bump the root to version 2. We sign the root metadata with both + // key 1 and 2, but the builder should only trust key 2. + 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.database().trusted_root().version(), 2); + assert_eq!( + client.database().trusted_root().consistent_snapshot(), + consistent_snapshot + ); + assert_eq!( + client + .database() + .trusted_root() + .root_keys() + .collect::>(), + vec![KEYS[2].public()], + ); + + // Another update should not fetch anything. + assert!(!client.update().await.unwrap()); + assert_eq!(client.database().trusted_root().version(), 2); + + // Now bump the root to version 3. The metadata will only be signed with + // key 2, and trusted by key 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.database().trusted_root().version(), 3); + assert_eq!( + client + .database() + .trusted_root() + .root_keys() + .collect::>(), + vec![KEYS[2].public()], + ); + + // Another update should not fetch anything. + assert!(!client.update().await.unwrap()); + assert_eq!(client.database().trusted_root().version(), 3); + } + + #[test] + fn test_from_database_root_must_be_one_after_the_last() { + block_on(async { + let mut repo = EphemeralRepository::::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::AttemptedMetadataRollBack { + role, + trusted_version: 1, + new_version: 3, + }) + if role == MetadataPath::root() + ); + }) + } + + #[test] + fn test_add_target_not_consistent_snapshot() { + block_on(async move { + let mut repo = EphemeralRepository::::new(); + + let hash_algs = &[HashAlgorithm::Sha256, HashAlgorithm::Sha512]; + + let target_path1 = TargetPath::new("foo/default").unwrap(); + let target_path1_hashed = TargetPath::new( + "foo/522dd05a607a520657daa19c061a0271224030307117c2e661505e14601d1e44.default", + ) + .unwrap(); + let target_file1: &[u8] = b"things fade, alternatives exclude"; + + let target_path2 = TargetPath::new("foo/custom").unwrap(); + let target_path2_hashed = TargetPath::new( + "foo/b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9.custom", + ) + .unwrap(); + let target_file2: &[u8] = b"hello world"; + + 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]]) + .add_target(target_path1.clone(), Cursor::new(target_file1)) + .await + .unwrap() + .target_hash_algorithms(hash_algs) + .add_target(target_path2.clone(), Cursor::new(target_file2)) + .await + .unwrap() + .commit() + .await + .unwrap(); + + // Make sure the targets were written correctly. + let mut rdr = repo.fetch_target(&target_path1_hashed).await.unwrap(); + let mut buf = vec![]; + rdr.read_to_end(&mut buf).await.unwrap(); + drop(rdr); + + assert_eq!(&buf, target_file1); + + let mut rdr = repo.fetch_target(&target_path2_hashed).await.unwrap(); + let mut buf = vec![]; + rdr.read_to_end(&mut buf).await.unwrap(); + drop(rdr); + + assert_eq!(&buf, target_file2); + + let mut client = Client::with_trusted_root( + Config::default(), + metadata.root().unwrap(), + EphemeralRepository::new(), + repo, + ) + .await + .unwrap(); + + client.update().await.unwrap(); + + // Make sure the target descriptions are correct. + assert_eq!( + client + .fetch_target_description(&target_path1) + .await + .unwrap(), + TargetDescription::from_slice(target_file1, &[HashAlgorithm::Sha256]).unwrap(), + ); + + assert_eq!( + client + .fetch_target_description(&target_path2) + .await + .unwrap(), + TargetDescription::from_slice(target_file2, hash_algs).unwrap(), + ); + + // Make sure we can fetch the targets. + let mut rdr = client.fetch_target(&target_path1).await.unwrap(); + let mut buf = vec![]; + rdr.read_to_end(&mut buf).await.unwrap(); + assert_eq!(&buf, target_file1); + drop(rdr); + + let mut rdr = client.fetch_target(&target_path2).await.unwrap(); + let mut buf = vec![]; + rdr.read_to_end(&mut buf).await.unwrap(); + assert_eq!(&buf, target_file2); + }) + } + + #[test] + fn test_add_target_consistent_snapshot() { + block_on(async move { + let mut repo = EphemeralRepository::::new(); + + let hash_algs = &[HashAlgorithm::Sha256, HashAlgorithm::Sha512]; + + let target_path1 = TargetPath::new("foo/bar").unwrap(); + let target_file1: &[u8] = b"things fade, alternatives exclude"; + + let target_path2 = TargetPath::new("baz").unwrap(); + let target_file2: &[u8] = b"hello world"; + let target_custom2 = hashmap! { + "hello".into() => "world".into(), + }; + + 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() + .target_hash_algorithms(hash_algs) + .add_target(target_path1.clone(), Cursor::new(target_file1)) + .await + .unwrap() + .add_target_with_custom( + target_path2.clone(), + Cursor::new(target_file2), + target_custom2.clone(), + ) + .await + .unwrap() + .commit() + .await + .unwrap(); + + // Make sure the target was written correctly with hash prefixes. + for (target_path, target_file) in &[ + (&target_path1, &target_file1), + (&target_path2, &target_file2), + ] { + 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(); + + // Make sure the target descriptions ar correct. + assert_eq!( + client + .fetch_target_description(&target_path1) + .await + .unwrap(), + TargetDescription::from_slice(target_file1, hash_algs).unwrap(), + ); + + assert_eq!( + client + .fetch_target_description(&target_path2) + .await + .unwrap(), + TargetDescription::from_slice_with_custom(target_file2, hash_algs, target_custom2) + .unwrap(), + ); + + // Make sure we can fetch the targets. + for (target_path, target_file) in &[ + (&target_path1, &target_file1), + (&target_path2, &target_file2), + ] { + 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::::new(); + + // First, write some metadata to the repo. + 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() + .stage_timestamp_with_builder(|builder| builder.expires(expires1)) + .unwrap() + .commit() + .await + .unwrap(); + + // We wrote all the metadata. + 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::root(), MetadataVersion::Number(1)), + metadata1.root().unwrap().as_bytes(), + ), + ( + (MetadataPath::root(), MetadataVersion::None), + metadata1.root().unwrap().as_bytes(), + ), + ( + (MetadataPath::targets(), MetadataVersion::Number(1)), + metadata1.targets().unwrap().as_bytes(), + ), + ( + (MetadataPath::targets(), MetadataVersion::None), + metadata1.targets().unwrap().as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::Number(1)), + metadata1.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::None), + metadata1.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::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(); + + // Next, write another batch, but only have the timestamp, snapshot, and targets keys. + 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() + .stage_timestamp_with_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::targets(), MetadataVersion::Number(2)), + metadata2.targets().unwrap().as_bytes(), + ), + ( + (MetadataPath::targets(), MetadataVersion::None), + metadata2.targets().unwrap().as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::Number(2)), + metadata2.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::None), + metadata2.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::timestamp(), MetadataVersion::None), + metadata2.timestamp().unwrap().as_bytes(), + ), + ] + .into_iter(), + ); + + assert_repo(&remote, &expected_metadata); + + // Now, only have the timestamp and snapshot keys online. + 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() + .stage_timestamp_with_builder(|builder| builder.expires(expires3)) + .unwrap() + .commit() + .await + .unwrap(); + + assert!(db.update_metadata(&metadata3).unwrap()); + + // We only have timestamp and snapshot. + 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::snapshot(), MetadataVersion::Number(3)), + metadata3.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::None), + metadata3.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::timestamp(), MetadataVersion::None), + metadata3.timestamp().unwrap().as_bytes(), + ), + ] + .into_iter(), + ); + + assert_repo(&remote, &expected_metadata); + + // Finally, only have the timestamp keys online. + 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() + .stage_timestamp_with_builder(|builder| builder.expires(expires4)) + .unwrap() + .commit() + .await + .unwrap(); + + assert!(db.update_metadata(&metadata4).unwrap()); + + // We only have timestamp and snapshot. + 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::timestamp(), MetadataVersion::None), + metadata4.timestamp().unwrap().as_bytes(), + )] + .into_iter(), + ); + + assert_repo(&remote, &expected_metadata); + }) + } + + #[test] + fn test_builder_inherits_from_trusted_targets() { + block_on(async move { + let mut repo = EphemeralRepository::::new(); + + let expires = Utc.ymd(2038, 1, 4).and_hms(0, 0, 0); + let hash_algs = &[HashAlgorithm::Sha256, HashAlgorithm::Sha512]; + let delegation_key = &KEYS[0]; + let delegation_path = MetadataPath::new("delegations").unwrap(); + + let target_path1 = TargetPath::new("target1").unwrap(); + let target_file1: &[u8] = b"target1 file"; + + let delegated_target_path1 = TargetPath::new("delegations/delegation1").unwrap(); + let delegated_target_file1: &[u8] = b"delegation1 file"; + + let delegation1 = Delegation::builder(delegation_path.clone()) + .key(delegation_key.public()) + .delegate_path(TargetPath::new("delegations/").unwrap()) + .build() + .unwrap(); + + let delegated_targets1 = TargetsMetadataBuilder::new() + .insert_target_from_slice( + delegated_target_path1, + delegated_target_file1, + &[HashAlgorithm::Sha256], + ) + .unwrap() + .signed::(delegation_key) + .unwrap(); + let raw_delegated_targets = delegated_targets1.to_raw().unwrap(); + + 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() + .unwrap() + .target_hash_algorithms(hash_algs) + .add_target(target_path1.clone(), Cursor::new(target_file1)) + .await + .unwrap() + .add_delegation_key(delegation_key.public().clone()) + .add_delegation_role(delegation1.clone()) + .stage_targets() + .unwrap() + .stage_snapshot_with_builder(|builder| { + builder.insert_metadata_description( + delegation_path.clone(), + MetadataDescription::from_slice( + raw_delegated_targets.as_bytes(), + 1, + &[HashAlgorithm::Sha256], + ) + .unwrap(), + ) + }) + .unwrap() + .commit() + .await + .unwrap(); + + // Next, create a new commit where we add a new target and delegation. This should copy + // over the old targets and delegations. + let mut database = Database::from_trusted_metadata(&metadata1).unwrap(); + + let target_path2 = TargetPath::new("bar").unwrap(); + let target_file2: &[u8] = b"bar file"; + + let delegated_target_path2 = TargetPath::new("delegations/delegation2").unwrap(); + let delegated_target_file2: &[u8] = b"delegation2 file"; + + let delegation2 = Delegation::builder(delegation_path.clone()) + .key(delegation_key.public()) + .delegate_path(TargetPath::new("delegations/").unwrap()) + .build() + .unwrap(); + + let delegated_targets2 = TargetsMetadataBuilder::new() + .insert_target_from_slice( + delegated_target_path2, + delegated_target_file2, + &[HashAlgorithm::Sha256], + ) + .unwrap() + .signed::(delegation_key) + .unwrap(); + let raw_delegated_targets = delegated_targets2.to_raw().unwrap(); + + let metadata2 = RepoBuilder::from_database(&mut repo, &database) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .stage_root() + .unwrap() + .target_hash_algorithms(hash_algs) + .add_target(target_path2.clone(), Cursor::new(target_file2)) + .await + .unwrap() + .add_delegation_role(delegation2.clone()) + .stage_targets_with_builder(|b| b.expires(expires)) + .unwrap() + .stage_snapshot_with_builder(|builder| { + builder.insert_metadata_description( + delegation_path.clone(), + MetadataDescription::from_slice( + raw_delegated_targets.as_bytes(), + 1, + &[HashAlgorithm::Sha256], + ) + .unwrap(), + ) + }) + .unwrap() + .commit() + .await + .unwrap(); + + database.update_metadata(&metadata2).unwrap(); + + assert_eq!( + &**database.trusted_targets().unwrap(), + &TargetsMetadataBuilder::new() + .version(2) + .expires(expires) + .insert_target_from_slice(target_path1.clone(), target_file1, hash_algs) + .unwrap() + .insert_target_from_slice(target_path2.clone(), target_file2, hash_algs) + .unwrap() + .delegations( + DelegationsBuilder::new() + .key(delegation_key.public().clone()) + .role(delegation1) + .role(delegation2) + .build() + .unwrap() + ) + .build() + .unwrap() + ) + }) + } + + #[test] + fn test_builder_rotating_keys_refreshes_metadata() { + block_on(async move { + let mut repo = EphemeralRepository::::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]]) + .commit() + .await + .unwrap(); + + let mut db = Database::from_trusted_metadata(&metadata1).unwrap(); + + // Because of [update-root], rotating any root keys should make a new timestamp and + // snapshot. + // + // FIXME(#297): This also purges targets, even though that's not conforming to the spec. + // + // [update-root]: https://theupdateframework.github.io/specification/v1.0.30/#update-root + let metadata2 = 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[1]]) + .commit() + .await + .unwrap(); + + assert!(metadata2.root().is_some()); + assert!(metadata2.targets().is_some()); + assert!(metadata2.snapshot().is_some()); + assert!(metadata2.timestamp().is_some()); + + db.update_metadata(&metadata2).unwrap(); + + assert_eq!(db.trusted_root().version(), 2); + assert_eq!(db.trusted_targets().unwrap().version(), 2); + assert_eq!(db.trusted_snapshot().unwrap().version(), 2); + assert_eq!(db.trusted_timestamp().unwrap().version(), 2); + + // Note that rotating the timestamp keys purges all the metadata, so add it back in. + + // Rotating the snapshot key should make new metadata. + let metadata3 = RepoBuilder::from_database(&mut repo, &db) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[1]]) + .trusted_timestamp_keys(&[&KEYS[1]]) + .commit() + .await + .unwrap(); + + assert!(metadata3.root().is_some()); + assert!(metadata2.targets().is_some()); + assert!(metadata2.snapshot().is_some()); + assert!(metadata2.timestamp().is_some()); + + db.update_metadata(&metadata3).unwrap(); + + assert_eq!(db.trusted_root().version(), 3); + assert_eq!(db.trusted_targets().unwrap().version(), 3); + assert_eq!(db.trusted_snapshot().unwrap().version(), 3); + assert_eq!(db.trusted_timestamp().unwrap().version(), 3); + + // Rotating the targets key should make a new targets, snapshot, and timestamp. + let metadata4 = RepoBuilder::from_database(&mut repo, &db) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[1]]) + .trusted_snapshot_keys(&[&KEYS[1]]) + .trusted_timestamp_keys(&[&KEYS[1]]) + .commit() + .await + .unwrap(); + + assert!(metadata4.root().is_some()); + assert!(metadata4.targets().is_some()); + assert!(metadata4.snapshot().is_some()); + assert!(metadata4.timestamp().is_some()); + + db.update_metadata(&metadata4).unwrap(); + + assert_eq!(db.trusted_root().version(), 4); + assert_eq!(db.trusted_targets().unwrap().version(), 4); + assert_eq!(db.trusted_snapshot().unwrap().version(), 4); + assert_eq!(db.trusted_timestamp().unwrap().version(), 4); + + // Rotating the root key should make a new targets, snapshot, and timestamp. + let metadata5 = RepoBuilder::from_database(&mut repo, &db) + .signing_root_keys(&[&KEYS[0]]) + .trusted_root_keys(&[&KEYS[1]]) + .trusted_targets_keys(&[&KEYS[1]]) + .trusted_snapshot_keys(&[&KEYS[1]]) + .trusted_timestamp_keys(&[&KEYS[1]]) + .commit() + .await + .unwrap(); + + assert!(metadata5.root().is_some()); + assert!(metadata5.targets().is_some()); + assert!(metadata5.snapshot().is_some()); + assert!(metadata5.timestamp().is_some()); + + db.update_metadata(&metadata5).unwrap(); + + assert_eq!(db.trusted_root().version(), 5); + assert_eq!(db.trusted_targets().unwrap().version(), 5); + assert_eq!(db.trusted_snapshot().unwrap().version(), 5); + assert_eq!(db.trusted_timestamp().unwrap().version(), 5); + }) + } + + #[test] + fn test_builder_expired_metadata_refreshes_metadata() { + block_on(async move { + let mut repo = EphemeralRepository::::new(); + + let epoch = DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc); + let root_expires = epoch + Duration::seconds(40); + let targets_expires = epoch + Duration::seconds(30); + let snapshot_expires = epoch + Duration::seconds(20); + let timestamp_expires = epoch + Duration::seconds(10); + + let current_time = epoch; + let metadata1 = RepoBuilder::create(&mut repo) + .current_time(current_time) + .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.expires(root_expires)) + .unwrap() + .stage_targets_with_builder(|builder| builder.expires(targets_expires)) + .unwrap() + .stage_snapshot_with_builder(|builder| builder.expires(snapshot_expires)) + .unwrap() + .stage_timestamp_with_builder(|builder| builder.expires(timestamp_expires)) + .unwrap() + .commit() + .await + .unwrap(); + + let mut db = + Database::from_trusted_metadata_with_start_time(&metadata1, ¤t_time).unwrap(); + + // Advance time to past the timestamp expiration. + let current_time = timestamp_expires + Duration::seconds(1); + let metadata2 = RepoBuilder::from_database(&mut repo, &db) + .current_time(current_time) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .commit() + .await + .unwrap(); + + assert!(metadata2.root().is_none()); + assert!(metadata2.targets().is_none()); + assert!(metadata2.snapshot().is_none()); + assert!(metadata2.timestamp().is_some()); + + db.update_metadata_with_start_time(&metadata2, ¤t_time) + .unwrap(); + + assert_eq!(db.trusted_root().version(), 1); + assert_eq!(db.trusted_targets().unwrap().version(), 1); + assert_eq!(db.trusted_snapshot().unwrap().version(), 1); + assert_eq!(db.trusted_timestamp().unwrap().version(), 2); + + // Advance time to past the snapshot expiration. + let current_time = snapshot_expires + Duration::seconds(1); + let metadata3 = RepoBuilder::from_database(&mut repo, &db) + .current_time(current_time) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .commit() + .await + .unwrap(); + + assert!(metadata3.root().is_none()); + assert!(metadata3.targets().is_none()); + assert!(metadata3.snapshot().is_some()); + assert!(metadata3.timestamp().is_some()); + + db.update_metadata_with_start_time(&metadata3, ¤t_time) + .unwrap(); + + assert_eq!(db.trusted_root().version(), 1); + assert_eq!(db.trusted_targets().unwrap().version(), 1); + assert_eq!(db.trusted_snapshot().unwrap().version(), 2); + assert_eq!(db.trusted_timestamp().unwrap().version(), 3); + + // Advance time to past the targets expiration. + let current_time = targets_expires + Duration::seconds(1); + let metadata4 = RepoBuilder::from_database(&mut repo, &db) + .current_time(current_time) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .commit() + .await + .unwrap(); + + assert!(metadata4.root().is_none()); + assert!(metadata4.targets().is_some()); + assert!(metadata4.snapshot().is_some()); + assert!(metadata4.timestamp().is_some()); + + db.update_metadata_with_start_time(&metadata4, ¤t_time) + .unwrap(); + + assert_eq!(db.trusted_root().version(), 1); + assert_eq!(db.trusted_targets().unwrap().version(), 2); + assert_eq!(db.trusted_snapshot().unwrap().version(), 3); + assert_eq!(db.trusted_timestamp().unwrap().version(), 4); + + // Advance time to past the root expiration. + // + // Because of [update-root], rotating any root keys should make a new timestamp and + // snapshot. + // + // [update-root]: https://theupdateframework.github.io/specification/v1.0.30/#update-root + let current_time = root_expires + Duration::seconds(1); + let metadata5 = RepoBuilder::from_database(&mut repo, &db) + .current_time(current_time) + .trusted_root_keys(&[&KEYS[0]]) + .trusted_targets_keys(&[&KEYS[0]]) + .trusted_snapshot_keys(&[&KEYS[0]]) + .trusted_timestamp_keys(&[&KEYS[0]]) + .commit() + .await + .unwrap(); + + assert!(metadata5.root().is_some()); + assert!(metadata5.targets().is_some()); + assert!(metadata5.snapshot().is_some()); + assert!(metadata5.timestamp().is_some()); + + db.update_metadata_with_start_time(&metadata5, ¤t_time) + .unwrap(); + + assert_eq!(db.trusted_root().version(), 2); + assert_eq!(db.trusted_targets().unwrap().version(), 3); + assert_eq!(db.trusted_snapshot().unwrap().version(), 4); + assert_eq!(db.trusted_timestamp().unwrap().version(), 5); + }) + } + + #[test] + fn test_adding_target_refreshes_metadata() { + block_on(async move { + let mut repo = EphemeralRepository::::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]]) + .commit() + .await + .unwrap(); + + let mut db = Database::from_trusted_metadata(&metadata1).unwrap(); + + let target_path = TargetPath::new("foo").unwrap(); + let target_file: &[u8] = b"foo file"; + + let metadata2 = 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]]) + .add_target(target_path, Cursor::new(target_file)) + .await + .unwrap() + .commit() + .await + .unwrap(); + + assert!(metadata2.root().is_none()); + assert!(metadata2.targets().is_some()); + assert!(metadata2.snapshot().is_some()); + assert!(metadata2.timestamp().is_some()); + + db.update_metadata(&metadata2).unwrap(); + + assert_eq!(db.trusted_root().version(), 1); + assert_eq!(db.trusted_targets().unwrap().version(), 2); + assert_eq!(db.trusted_snapshot().unwrap().version(), 2); + assert_eq!(db.trusted_timestamp().unwrap().version(), 2); + }) + } +} diff --git a/vendor/tuf/src/repository.rs b/vendor/tuf/src/repository.rs new file mode 100644 index 0000000000..3b314a3678 --- /dev/null +++ b/vendor/tuf/src/repository.rs @@ -0,0 +1,744 @@ +//! Interfaces for interacting with different types of TUF repositories. + +use crate::crypto::{self, HashAlgorithm, HashValue}; +use crate::interchange::DataInterchange; +use crate::metadata::{ + Metadata, MetadataPath, MetadataVersion, RawSignedMetadata, TargetDescription, TargetPath, +}; +use crate::util::SafeAsyncRead; +use crate::{Error, Result}; + +use futures_io::AsyncRead; +use futures_util::future::BoxFuture; +use futures_util::io::AsyncReadExt; +use std::marker::PhantomData; +use std::sync::Arc; + +mod file_system; +pub use self::file_system::{ + FileSystemBatchUpdate, FileSystemRepository, FileSystemRepositoryBuilder, +}; + +#[cfg(feature = "hyper")] +mod http; + +#[cfg(feature = "hyper")] +pub use self::http::{HttpRepository, HttpRepositoryBuilder}; + +mod ephemeral; +pub use self::ephemeral::{EphemeralBatchUpdate, EphemeralRepository}; + +#[cfg(test)] +mod error_repo; +#[cfg(test)] +pub(crate) use self::error_repo::ErrorRepository; + +#[cfg(test)] +mod track_repo; +#[cfg(test)] +pub(crate) use self::track_repo::{Track, TrackRepository}; + +/// A readable TUF repository. +pub trait RepositoryProvider +where + D: DataInterchange + Sync, +{ + /// Fetch signed metadata identified by `meta_path`, `version`, and + /// [`D::extension()`][extension]. + /// + /// Implementations may ignore `max_length` and `hash_data` as [`Client`][Client] will verify + /// these constraints itself. However, it may be more efficient for an implementation to detect + /// invalid metadata and fail the fetch operation before streaming all of the bytes of the + /// metadata. + /// + /// [extension]: crate::interchange::DataInterchange::extension + /// [Client]: crate::client::Client + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>>; + + /// Fetch the given target. + /// + /// Implementations may ignore the `length` and `hashes` fields in `target_description` as + /// [`Client`][Client] will verify these constraints itself. However, it may be more efficient + /// for an implementation to detect invalid targets and fail the fetch operation before + /// streaming all of the bytes. + /// + /// [Client]: crate::client::Client + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>>; +} + +/// Test helper to help read a metadata file from a repository into a string. +#[cfg(test)] +pub(crate) async fn fetch_metadata_to_string( + repo: &R, + meta_path: &MetadataPath, + version: MetadataVersion, +) -> Result +where + D: DataInterchange + Sync, + R: RepositoryProvider, +{ + let mut reader = repo.fetch_metadata(meta_path, version).await?; + let mut buf = String::new(); + reader.read_to_string(&mut buf).await.unwrap(); + Ok(buf) +} + +/// Test helper to help read a target file from a repository into a string. +#[cfg(test)] +pub(crate) async fn fetch_target_to_string( + repo: &R, + target_path: &TargetPath, +) -> Result +where + D: DataInterchange + Sync, + R: RepositoryProvider, +{ + let mut reader = repo.fetch_target(target_path).await?; + let mut buf = String::new(); + reader.read_to_string(&mut buf).await.unwrap(); + Ok(buf) +} + +/// A writable TUF repository. Most implementors of this trait should also implement +/// `RepositoryProvider`. +pub trait RepositoryStorage: Send +where + D: DataInterchange + Sync, +{ + /// Store the provided `metadata` in a location identified by `meta_path`, `version`, and + /// [`D::extension()`][extension], overwriting any existing metadata at that location. + /// + /// [extension]: crate::interchange::DataInterchange::extension + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>>; + + /// Store the provided `target` in a location identified by `target_path`, overwriting any + /// existing target at that location. + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>>; +} + +/// A subtrait of both RepositoryStorage and RepositoryProvider. This is useful to create +/// trait objects that implement both traits. +pub trait RepositoryStorageProvider: RepositoryStorage + RepositoryProvider +where + D: DataInterchange + Sync, +{ +} + +impl RepositoryStorageProvider for T +where + D: DataInterchange + Sync, + T: RepositoryStorage + RepositoryProvider, +{ +} + +impl RepositoryProvider for &T +where + T: RepositoryProvider, + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_metadata(meta_path, version) + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_target(target_path) + } +} + +impl RepositoryProvider for &mut T +where + T: RepositoryProvider, + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_metadata(meta_path, version) + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_target(target_path) + } +} + +impl RepositoryStorage for &mut T +where + T: RepositoryStorage, + D: DataInterchange + Sync, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + (**self).store_metadata(meta_path, version, metadata) + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + (**self).store_target(target_path, target) + } +} + +impl RepositoryStorage for Box +where + T: RepositoryStorage + ?Sized, + D: DataInterchange + Sync, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + (**self).store_metadata(meta_path, version, metadata) + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + (**self).store_target(target_path, target) + } +} + +impl RepositoryProvider for Box +where + T: RepositoryProvider + ?Sized, + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_metadata(meta_path, version) + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_target(target_path) + } +} + +impl RepositoryProvider for Arc +where + D: DataInterchange + Sync, + T: RepositoryProvider + ?Sized, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_metadata(meta_path, version) + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_target(target_path) + } +} + +impl RepositoryProvider for &dyn RepositoryProvider +where + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_metadata(meta_path, version) + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_target(target_path) + } +} + +impl RepositoryProvider for &mut dyn RepositoryProvider +where + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_metadata(meta_path, version) + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + (**self).fetch_target(target_path) + } +} + +impl RepositoryStorage for &mut dyn RepositoryStorage +where + D: DataInterchange + Sync, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + (**self).store_metadata(meta_path, version, metadata) + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + (**self).store_target(target_path, target) + } +} + +/// A wrapper around an implementation of [`RepositoryProvider`] and/or [`RepositoryStorage`] tied +/// to a specific [`DataInterchange`](crate::interchange::DataInterchange) that will enforce +/// provided length limits and hash checks. +#[derive(Debug, Clone)] +pub(crate) struct Repository { + repository: R, + _interchange: PhantomData, +} + +impl Repository { + /// Creates a new [`Repository`] wrapping `repository`. + pub(crate) fn new(repository: R) -> Self { + Self { + repository, + _interchange: PhantomData, + } + } + + /// Perform a sanity check that `M`, `Role`, and `MetadataPath` all describe the same entity. + fn check(meta_path: &MetadataPath) -> Result<()> + where + M: Metadata, + { + if !M::ROLE.fuzzy_matches_path(meta_path) { + return Err(Error::IllegalArgument(format!( + "Role {} does not match path {:?}", + M::ROLE, + meta_path + ))); + } + + Ok(()) + } + + pub(crate) fn into_inner(self) -> R { + self.repository + } + + pub(crate) fn as_inner(&self) -> &R { + &self.repository + } + + pub(crate) fn as_inner_mut(&mut self) -> &mut R { + &mut self.repository + } +} + +impl Repository +where + R: RepositoryProvider, + D: DataInterchange + Sync, +{ + /// Fetch metadata identified by `meta_path`, `version`, and [`D::extension()`][extension]. + /// + /// If `max_length` is provided, this method will return an error if the metadata exceeds + /// `max_length` bytes. If `hash_data` is provided, this method will return and error if the + /// hashed bytes of the metadata do not match `hash_data`. + /// + /// [extension]: crate::interchange::DataInterchange::extension + pub(crate) async fn fetch_metadata<'a, M>( + &'a self, + meta_path: &'a MetadataPath, + version: MetadataVersion, + max_length: Option, + hashes: Vec<(&'static HashAlgorithm, HashValue)>, + ) -> Result> + where + M: Metadata, + { + Self::check::(meta_path)?; + + // Fetch the metadata, verifying max_length and hashes (if provided), as + // the repository implementation should only be trusted to use those as + // hints to fail early. + let mut reader = self + .repository + .fetch_metadata(meta_path, version) + .await? + .check_length_and_hash(max_length.unwrap_or(::std::usize::MAX) as u64, hashes)?; + + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).await?; + + Ok(RawSignedMetadata::new(buf)) + } + + /// Fetch the target identified by `target_path` through the returned `AsyncRead`, verifying + /// that the target matches the preferred hash specified in `target_description` and that it is + /// the expected length. Such verification errors will be provided by a read failure on the + /// provided `AsyncRead`. + /// + /// It is **critical** that none of the bytes from the returned `AsyncRead` are used until it + /// has been fully consumed as the data is untrusted. + pub(crate) async fn fetch_target( + &self, + consistent_snapshot: bool, + target_path: &TargetPath, + target_description: TargetDescription, + ) -> Result { + // https://theupdateframework.github.io/specification/v1.0.26/#fetch-target 5.7.3: + // + // [...] download the target (up to the number of bytes specified in the targets metadata), + // and verify that its hashes match the targets metadata. + let length = target_description.length(); + let hashes = crypto::retain_supported_hashes(target_description.hashes()); + if hashes.is_empty() { + return Err(Error::NoSupportedHashAlgorithm); + } + + // https://theupdateframework.github.io/specification/v1.0.26/#fetch-target 5.7.3: + // + // [...] If consistent snapshots are not used (see § 6.2 Consistent snapshots), then the + // filename used to download the target file is of the fixed form FILENAME.EXT (e.g., + // foobar.tar.gz). Otherwise, the filename is of the form HASH.FILENAME.EXT [...] + let target = if consistent_snapshot { + let mut hashes = hashes.iter(); + loop { + if let Some((_, hash)) = hashes.next() { + let target_path = target_path.with_hash_prefix(hash)?; + match self.repository.fetch_target(&target_path).await { + Ok(target) => break target, + Err(Error::TargetNotFound(_)) => {} + Err(err) => return Err(err), + } + } else { + return Err(Error::TargetNotFound(target_path.clone())); + } + } + } else { + self.repository.fetch_target(target_path).await? + }; + + target.check_length_and_hash(length, hashes) + } +} + +impl Repository +where + R: RepositoryStorage, + D: DataInterchange + Sync, +{ + /// Store the provided `metadata` in a location identified by `meta_path`, `version`, and + /// [`D::extension()`][extension], overwriting any existing metadata at that location. + /// + /// [extension]: crate::interchange::DataInterchange::extension + pub async fn store_metadata<'a, M>( + &'a mut self, + path: &'a MetadataPath, + version: MetadataVersion, + metadata: &'a RawSignedMetadata, + ) -> Result<()> + where + M: Metadata + Sync, + { + Self::check::(path)?; + + self.repository + .store_metadata(path, version, &mut metadata.as_bytes()) + .await + } + + /// Store the provided `target` in a location identified by `target_path`. + pub async fn store_target<'a>( + &'a mut self, + target_path: &'a TargetPath, + target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> Result<()> { + self.repository.store_target(target_path, target).await + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::interchange::Json; + use crate::metadata::{MetadataPath, MetadataVersion, RootMetadata, SnapshotMetadata}; + use crate::repository::EphemeralRepository; + use assert_matches::assert_matches; + use futures_executor::block_on; + + #[test] + fn repository_forwards_not_found_error() { + block_on(async { + let repo = Repository::<_, Json>::new(EphemeralRepository::new()); + + assert_matches!( + repo.fetch_metadata::( + &MetadataPath::root(), + MetadataVersion::None, + None, + vec![], + ) + .await, + Err(Error::MetadataNotFound { path, version }) + if path == MetadataPath::root() && version == MetadataVersion::None + ); + }); + } + + #[test] + fn repository_rejects_mismatched_path() { + block_on(async { + let mut repo = Repository::<_, Json>::new(EphemeralRepository::new()); + let fake_metadata = RawSignedMetadata::::new(vec![]); + + repo.store_metadata(&MetadataPath::root(), MetadataVersion::None, &fake_metadata) + .await + .unwrap(); + + assert_matches!( + repo.store_metadata( + &MetadataPath::snapshot(), + MetadataVersion::None, + &fake_metadata, + ) + .await, + Err(Error::IllegalArgument(_)) + ); + + assert_matches!( + repo.fetch_metadata::( + &MetadataPath::root(), + MetadataVersion::None, + None, + vec![], + ) + .await, + Err(Error::IllegalArgument(_)) + ); + }); + } + + #[test] + fn repository_verifies_metadata_hash() { + block_on(async { + let path = MetadataPath::root(); + let version = MetadataVersion::None; + let data: &[u8] = b"valid metadata"; + let _metadata = RawSignedMetadata::::new(data.to_vec()); + let data_hash = crypto::calculate_hash(data, &HashAlgorithm::Sha256); + + let mut repo = EphemeralRepository::new(); + repo.store_metadata(&path, version, &mut &*data) + .await + .unwrap(); + + let client = Repository::<_, Json>::new(repo); + + assert_matches!( + client + .fetch_metadata::( + &path, + version, + None, + vec![(&HashAlgorithm::Sha256, data_hash)], + ) + .await, + Ok(_metadata) + ); + }) + } + + #[test] + fn repository_rejects_corrupt_metadata() { + block_on(async { + let path = MetadataPath::root(); + let version = MetadataVersion::None; + let data: &[u8] = b"corrupt metadata"; + + let mut repo = EphemeralRepository::new(); + repo.store_metadata(&path, version, &mut &*data) + .await + .unwrap(); + + let client = Repository::<_, Json>::new(repo); + + assert_matches!( + client + .fetch_metadata::( + &path, + version, + None, + vec![(&HashAlgorithm::Sha256, HashValue::new(vec![]))], + ) + .await, + Err(_) + ); + }) + } + + #[test] + fn repository_verifies_metadata_size() { + block_on(async { + let path = MetadataPath::root(); + let version = MetadataVersion::None; + let data: &[u8] = b"reasonably sized metadata"; + let _metadata = RawSignedMetadata::::new(data.to_vec()); + + let mut repo = EphemeralRepository::new(); + repo.store_metadata(&path, version, &mut &*data) + .await + .unwrap(); + + let client = Repository::<_, Json>::new(repo); + + assert_matches!( + client + .fetch_metadata::(&path, version, Some(100), vec![]) + .await, + Ok(_metadata) + ); + }) + } + + #[test] + fn repository_rejects_oversized_metadata() { + block_on(async { + let path = MetadataPath::root(); + let version = MetadataVersion::None; + let data: &[u8] = b"very big metadata"; + + let mut repo = EphemeralRepository::new(); + repo.store_metadata(&path, version, &mut &*data) + .await + .unwrap(); + + let client = Repository::<_, Json>::new(repo); + + assert_matches!( + client + .fetch_metadata::(&path, version, Some(4), vec![]) + .await, + Err(_) + ); + }) + } + + #[test] + fn repository_rejects_corrupt_targets() { + block_on(async { + let repo = EphemeralRepository::new(); + let mut client = Repository::<_, Json>::new(repo); + + let data: &[u8] = b"like tears in the rain"; + let target_description = + TargetDescription::from_slice(data, &[HashAlgorithm::Sha256]).unwrap(); + let path = TargetPath::new("batty").unwrap(); + client.store_target(&path, &mut &*data).await.unwrap(); + + let mut read = client + .fetch_target(false, &path, target_description.clone()) + .await + .unwrap(); + let mut buf = Vec::new(); + read.read_to_end(&mut buf).await.unwrap(); + assert_eq!(buf.as_slice(), data); + drop(read); + + let bad_data: &[u8] = b"you're in a desert"; + client.store_target(&path, &mut &*bad_data).await.unwrap(); + let mut read = client + .fetch_target(false, &path, target_description) + .await + .unwrap(); + assert!(read.read_to_end(&mut buf).await.is_err()); + }) + } + + #[test] + fn repository_takes_trait_objects() { + block_on(async { + let repo: Box> = + Box::new(EphemeralRepository::new()); + let mut client = Repository::<_, Json>::new(repo); + + let data: &[u8] = b"like tears in the rain"; + let target_description = + TargetDescription::from_slice(data, &[HashAlgorithm::Sha256]).unwrap(); + let path = TargetPath::new("batty").unwrap(); + client.store_target(&path, &mut &*data).await.unwrap(); + + let mut read = client + .fetch_target(false, &path, target_description) + .await + .unwrap(); + let mut buf = Vec::new(); + read.read_to_end(&mut buf).await.unwrap(); + assert_eq!(buf.as_slice(), data); + }) + } + + #[test] + fn repository_dyn_impls_repository_traits() { + let mut repo = EphemeralRepository::new(); + + fn storage>(_t: T) {} + fn provider>(_t: T) {} + + provider(&repo as &dyn RepositoryProvider); + provider(&mut repo as &mut dyn RepositoryProvider); + storage(&mut repo as &mut dyn RepositoryStorage); + } +} diff --git a/vendor/tuf/src/repository/ephemeral.rs b/vendor/tuf/src/repository/ephemeral.rs new file mode 100644 index 0000000000..ec03103584 --- /dev/null +++ b/vendor/tuf/src/repository/ephemeral.rs @@ -0,0 +1,424 @@ +//! Repository implementation backed by memory + +use futures_io::AsyncRead; +use futures_util::future::{BoxFuture, FutureExt}; +use futures_util::io::{AsyncReadExt, Cursor}; +use std::collections::HashMap; +use std::marker::PhantomData; + +use crate::error::Error; +use crate::interchange::DataInterchange; +use crate::metadata::{MetadataPath, MetadataVersion, TargetPath}; +use crate::repository::{RepositoryProvider, RepositoryStorage}; +use crate::Result; + +/// An ephemeral repository contained solely in memory. +#[derive(Debug, Default)] +pub struct EphemeralRepository { + metadata: HashMap<(MetadataPath, MetadataVersion), Box<[u8]>>, + targets: HashMap>, + _interchange: PhantomData, +} + +impl EphemeralRepository +where + D: DataInterchange, +{ + /// Create a new ephemeral repository. + pub fn new() -> Self { + Self { + metadata: HashMap::new(), + targets: HashMap::new(), + _interchange: PhantomData, + } + } + + /// Returns a [EphemeralBatchUpdate] for manipulating this repository. This allows callers to + /// stage a number of mutations, and optionally atomically write them all at once. + pub fn batch_update(&mut self) -> EphemeralBatchUpdate<'_, D> { + EphemeralBatchUpdate { + parent_repo: self, + staging_repo: EphemeralRepository::new(), + } + } + + #[cfg(test)] + pub(crate) fn metadata(&self) -> &HashMap<(MetadataPath, MetadataVersion), Box<[u8]>> { + &self.metadata + } + + /// Synchronously fetch the raw bytes of a stored target, without going + /// through the async [`RepositoryProvider`] trait. + /// + /// Since an [`EphemeralRepository`] is fully in-memory, no I/O is required + /// to read the target and no async runtime is needed. Returns + /// [`Error::TargetNotFound`] if the target has not been stored. + pub fn get_target(&self, target_path: &TargetPath) -> Result<&[u8]> { + self.targets + .get(target_path) + .map(|b| &**b) + .ok_or_else(|| Error::TargetNotFound(target_path.clone())) + } +} + +impl RepositoryProvider for EphemeralRepository +where + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + let bytes = match self.metadata.get(&(meta_path.clone(), version)) { + Some(bytes) => Ok(bytes), + None => Err(Error::MetadataNotFound { + path: meta_path.clone(), + version, + }), + }; + bytes_to_reader(bytes).boxed() + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + let bytes = match self.targets.get(target_path) { + Some(bytes) => Ok(bytes), + None => Err(Error::TargetNotFound(target_path.clone())), + }; + bytes_to_reader(bytes).boxed() + } +} + +impl RepositoryStorage for EphemeralRepository +where + D: DataInterchange + Sync, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + let meta_path = meta_path.clone(); + let self_metadata = &mut self.metadata; + async move { + let mut buf = Vec::new(); + metadata.read_to_end(&mut buf).await?; + buf.shrink_to_fit(); + self_metadata.insert((meta_path, version), buf.into_boxed_slice()); + Ok(()) + } + .boxed() + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + read: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + let target_path = target_path.clone(); + let self_targets = &mut self.targets; + async move { + let mut buf = Vec::new(); + read.read_to_end(&mut buf).await?; + buf.shrink_to_fit(); + self_targets.insert(target_path.clone(), buf.into_boxed_slice()); + Ok(()) + } + .boxed() + } +} + +/// [EphemeralBatchUpdate] is a special repository that is designed to write the metadata and +/// targets to an [EphemeralRepository] in a single batch. +/// +/// Note: `EphemeralBatchUpdate::commit()` must be called in order to write the metadata and +/// targets to the [EphemeralRepository]. Otherwise any queued changes will be lost on drop. +#[derive(Debug)] +pub struct EphemeralBatchUpdate<'a, D> { + parent_repo: &'a mut EphemeralRepository, + staging_repo: EphemeralRepository, +} + +impl<'a, D> EphemeralBatchUpdate<'a, D> +where + D: DataInterchange + Sync, +{ + /// Synchronously fetch the raw bytes of a stored target, without going + /// through the async [`RepositoryProvider`] trait. + /// + /// The staged (uncommitted) targets shadow the parent's targets, matching + /// the behavior of [`RepositoryProvider::fetch_target`]. + pub fn get_target(&self, target_path: &TargetPath) -> Result<&[u8]> { + if let Some(bytes) = self.staging_repo.targets.get(target_path) { + Ok(&**bytes) + } else { + self.parent_repo.get_target(target_path) + } + } + + /// Write all the metadata and targets in the [EphemeralBatchUpdate] to the source + /// [EphemeralRepository] in a single batch operation. + pub fn commit(self) { + self.parent_repo + .metadata + .extend(self.staging_repo.metadata.into_iter()); + + self.parent_repo + .targets + .extend(self.staging_repo.targets.into_iter()); + } +} + +impl RepositoryProvider for EphemeralBatchUpdate<'_, D> +where + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + let key = (meta_path.clone(), version); + let bytes = if let Some(bytes) = self.staging_repo.metadata.get(&key) { + Ok(bytes) + } else { + self.parent_repo + .metadata + .get(&key) + .ok_or_else(|| Error::MetadataNotFound { + path: meta_path.clone(), + version, + }) + }; + bytes_to_reader(bytes).boxed() + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + let bytes = if let Some(bytes) = self.staging_repo.targets.get(target_path) { + Ok(bytes) + } else { + self.parent_repo + .targets + .get(target_path) + .ok_or_else(|| Error::TargetNotFound(target_path.clone())) + }; + bytes_to_reader(bytes).boxed() + } +} + +impl RepositoryStorage for EphemeralBatchUpdate<'_, D> +where + D: DataInterchange + Sync, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + self.staging_repo + .store_metadata(meta_path, version, metadata) + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + read: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + self.staging_repo.store_target(target_path, read) + } +} + +#[allow(clippy::borrowed_box)] +async fn bytes_to_reader( + bytes: Result<&'_ Box<[u8]>>, +) -> Result> { + let bytes = bytes?; + let reader: Box = Box::new(Cursor::new(bytes)); + Ok(reader) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::interchange::Json; + use crate::repository::{fetch_metadata_to_string, fetch_target_to_string}; + use assert_matches::assert_matches; + use futures_executor::block_on; + + #[test] + fn ephemeral_repo_targets() { + block_on(async { + let mut repo = EphemeralRepository::::new(); + + let path = TargetPath::new("batty").unwrap(); + if let Err(err) = repo.fetch_target(&path).await { + assert_matches!(err, Error::TargetNotFound(p) if p == path); + } else { + panic!("expected fetch_target to fail"); + } + + let data: &[u8] = b"like tears in the rain"; + let path = TargetPath::new("batty").unwrap(); + repo.store_target(&path, &mut &*data).await.unwrap(); + + let mut read = repo.fetch_target(&path).await.unwrap(); + let mut buf = Vec::new(); + read.read_to_end(&mut buf).await.unwrap(); + assert_eq!(buf.as_slice(), data); + drop(read); + + // RepositoryProvider implementations do not guarantee data is not corrupt. + let bad_data: &[u8] = b"you're in a desert"; + repo.store_target(&path, &mut &*bad_data).await.unwrap(); + let mut read = repo.fetch_target(&path).await.unwrap(); + buf.clear(); + read.read_to_end(&mut buf).await.unwrap(); + assert_eq!(buf.as_slice(), bad_data); + }) + } + + #[test] + fn ephemeral_repo_get_target_sync() { + block_on(async { + let mut repo = EphemeralRepository::::new(); + let path = TargetPath::new("sync-target").unwrap(); + + // Missing target returns TargetNotFound. + assert_matches!( + repo.get_target(&path), + Err(Error::TargetNotFound(p)) if p == path + ); + + // After storing, we can read the bytes back synchronously. + let data: &[u8] = b"in-memory bytes"; + repo.store_target(&path, &mut &*data).await.unwrap(); + assert_eq!(repo.get_target(&path).unwrap(), data); + + // Batch update: staged bytes shadow the parent's bytes, and the + // parent's bytes remain visible for un-staged targets. + let other_path = TargetPath::new("other").unwrap(); + let other_data: &[u8] = b"parent only"; + repo.store_target(&other_path, &mut &*other_data) + .await + .unwrap(); + + let mut batch = repo.batch_update(); + let staged: &[u8] = b"staged bytes"; + batch.store_target(&path, &mut &*staged).await.unwrap(); + assert_eq!(batch.get_target(&path).unwrap(), staged); + assert_eq!(batch.get_target(&other_path).unwrap(), other_data); + + // Dropping the batch without committing leaves the parent unchanged. + drop(batch); + assert_eq!(repo.get_target(&path).unwrap(), data); + }) + } + + #[test] + fn ephemeral_repo_batch_update() { + block_on(async { + let mut repo = EphemeralRepository::::new(); + + let meta_path = MetadataPath::new("meta").unwrap(); + let meta_version = MetadataVersion::None; + let target_path = TargetPath::new("target").unwrap(); + + // First, write some stuff to the repository. + let committed_meta = "committed meta"; + let committed_target = "committed target"; + + repo.store_metadata(&meta_path, meta_version, &mut committed_meta.as_bytes()) + .await + .unwrap(); + + repo.store_target(&target_path, &mut committed_target.as_bytes()) + .await + .unwrap(); + + let mut batch = repo.batch_update(); + + // Make sure we can read back the committed stuff. + assert_eq!( + fetch_metadata_to_string(&batch, &meta_path, meta_version) + .await + .unwrap(), + committed_meta, + ); + assert_eq!( + fetch_target_to_string(&batch, &target_path).await.unwrap(), + committed_target, + ); + + // Next, stage some stuff in the batch_update. + let staged_meta = "staged meta"; + let staged_target = "staged target"; + batch + .store_metadata(&meta_path, meta_version, &mut staged_meta.as_bytes()) + .await + .unwrap(); + batch + .store_target(&target_path, &mut staged_target.as_bytes()) + .await + .unwrap(); + + // Make sure it got staged. + assert_eq!( + fetch_metadata_to_string(&batch, &meta_path, meta_version) + .await + .unwrap(), + staged_meta, + ); + assert_eq!( + fetch_target_to_string(&batch, &target_path).await.unwrap(), + staged_target, + ); + + // Next, drop the batch_update. We shouldn't have written the data back to the + // repository. + drop(batch); + + assert_eq!( + fetch_metadata_to_string(&repo, &meta_path, meta_version) + .await + .unwrap(), + committed_meta, + ); + assert_eq!( + fetch_target_to_string(&repo, &target_path).await.unwrap(), + committed_target, + ); + + // Do the batch_update again, but this time write the data. + let mut batch = repo.batch_update(); + batch + .store_metadata(&meta_path, meta_version, &mut staged_meta.as_bytes()) + .await + .unwrap(); + batch + .store_target(&target_path, &mut staged_target.as_bytes()) + .await + .unwrap(); + batch.commit(); + + // Make sure the new data got to the repository. + assert_eq!( + fetch_metadata_to_string(&repo, &meta_path, meta_version) + .await + .unwrap(), + staged_meta, + ); + assert_eq!( + fetch_target_to_string(&repo, &target_path).await.unwrap(), + staged_target, + ); + }) + } +} diff --git a/vendor/tuf/src/repository/error_repo.rs b/vendor/tuf/src/repository/error_repo.rs new file mode 100644 index 0000000000..ff52d676c5 --- /dev/null +++ b/vendor/tuf/src/repository/error_repo.rs @@ -0,0 +1,81 @@ +use { + crate::{ + interchange::DataInterchange, + metadata::{MetadataPath, MetadataVersion, TargetPath}, + repository::{RepositoryProvider, RepositoryStorage}, + Error, Result, + }, + futures_io::AsyncRead, + futures_util::future::{BoxFuture, FutureExt}, + std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +pub(crate) struct ErrorRepository { + repo: R, + fail_metadata_stores: Arc, +} + +impl ErrorRepository { + pub(crate) fn new(repo: R) -> Self { + Self { + repo, + fail_metadata_stores: Arc::new(AtomicBool::new(false)), + } + } + + pub(crate) fn fail_metadata_stores(&self, fail_metadata_stores: bool) { + self.fail_metadata_stores + .store(fail_metadata_stores, Ordering::SeqCst); + } +} + +impl RepositoryProvider for ErrorRepository +where + R: RepositoryProvider + Sync, + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + self.repo.fetch_metadata(meta_path, version) + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + self.repo.fetch_target(target_path) + } +} + +impl RepositoryStorage for ErrorRepository +where + R: RepositoryStorage + Sync, + D: DataInterchange + Sync, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + if self.fail_metadata_stores.load(Ordering::SeqCst) { + async { Err(Error::Encoding("failed".into())) }.boxed() + } else { + self.repo.store_metadata(meta_path, version, metadata) + } + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + self.repo.store_target(target_path, target) + } +} diff --git a/vendor/tuf/src/repository/file_system.rs b/vendor/tuf/src/repository/file_system.rs new file mode 100644 index 0000000000..6324200998 --- /dev/null +++ b/vendor/tuf/src/repository/file_system.rs @@ -0,0 +1,618 @@ +//! Repository implementation backed by a file system. + +use futures_io::AsyncRead; +use futures_util::future::{BoxFuture, FutureExt}; +use futures_util::io::{copy, AllowStdIo}; +use log::debug; +use std::collections::HashMap; +use std::fs::{DirBuilder, File}; +use std::io; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use tempfile::{NamedTempFile, TempPath}; + +use crate::error::{Error, Result}; +use crate::interchange::DataInterchange; +use crate::metadata::{MetadataPath, MetadataVersion, TargetPath}; +use crate::repository::{RepositoryProvider, RepositoryStorage}; + +/// A builder to create a repository contained on the local file system. +pub struct FileSystemRepositoryBuilder { + local_path: PathBuf, + metadata_prefix: Option, + targets_prefix: Option, + _interchange: PhantomData, +} + +impl FileSystemRepositoryBuilder +where + D: DataInterchange, +{ + /// Create a new repository with the given `local_path` prefix. + pub fn new>(local_path: P) -> Self { + FileSystemRepositoryBuilder { + local_path: local_path.into(), + metadata_prefix: None, + targets_prefix: None, + _interchange: PhantomData, + } + } + + /// The argument `metadata_prefix` is used to provide an alternate path where metadata is + /// stored on the repository. If `None`, this defaults to `/`. For example, if there is a TUF + /// repository at `/usr/local/repo/`, but all metadata is stored at `/usr/local/repo/meta/`, + /// then passing the arg `Some("meta".into())` would cause `root.json` to be fetched from + /// `/usr/local/repo/meta/root.json`. + pub fn metadata_prefix>(mut self, metadata_prefix: P) -> Self { + self.metadata_prefix = Some(metadata_prefix.into()); + self + } + + /// The argument `targets_prefix` is used to provide an alternate path where targets are + /// stored on the repository. If `None`, this defaults to `/`. For example, if there is a TUF + /// repository at `/usr/local/repo/`, but all targets are stored at `/usr/local/repo/targets/`, + /// then passing the arg `Some("targets".into())` would cause `hello-world` to be fetched from + /// `/usr/local/repo/targets/hello-world`. + pub fn targets_prefix>(mut self, targets_prefix: P) -> Self { + self.targets_prefix = Some(targets_prefix.into()); + self + } + + /// Build a `FileSystemRepository`. + pub fn build(self) -> Result> { + let metadata_path = if let Some(metadata_prefix) = self.metadata_prefix { + self.local_path.join(metadata_prefix) + } else { + self.local_path.clone() + }; + DirBuilder::new().recursive(true).create(&metadata_path)?; + + let targets_path = if let Some(targets_prefix) = self.targets_prefix { + self.local_path.join(targets_prefix) + } else { + self.local_path.clone() + }; + DirBuilder::new().recursive(true).create(&targets_path)?; + + Ok(FileSystemRepository { + metadata_path, + targets_path, + _interchange: PhantomData, + }) + } +} + +/// A repository contained on the local file system. +#[derive(Debug)] +pub struct FileSystemRepository +where + D: DataInterchange, +{ + metadata_path: PathBuf, + targets_path: PathBuf, + _interchange: PhantomData, +} + +impl FileSystemRepository +where + D: DataInterchange, +{ + /// Create a [FileSystemRepositoryBuilder]. + pub fn builder>(local_path: P) -> FileSystemRepositoryBuilder { + FileSystemRepositoryBuilder::new(local_path) + } + + /// Create a new repository on the local file system. + pub fn new>(local_path: P) -> Result { + FileSystemRepositoryBuilder::new(local_path) + .metadata_prefix("metadata") + .targets_prefix("targets") + .build() + } + + /// Returns a [FileSystemBatchUpdate] for manipulating this repository. This allows callers to + /// stage a number of mutations, and optionally write them all at once. + pub fn batch_update(&mut self) -> FileSystemBatchUpdate { + FileSystemBatchUpdate { + repo: self, + metadata: HashMap::new(), + targets: HashMap::new(), + } + } + + fn metadata_path(&self, meta_path: &MetadataPath, version: MetadataVersion) -> PathBuf { + let mut path = self.metadata_path.clone(); + path.extend(meta_path.components::(version)); + path + } + + fn target_path(&self, target_path: &TargetPath) -> PathBuf { + let mut path = self.targets_path.clone(); + path.extend(target_path.components()); + path + } + + fn fetch_metadata_from_path( + &self, + meta_path: &MetadataPath, + version: MetadataVersion, + path: &Path, + ) -> BoxFuture<'_, Result>> { + let reader = File::open(&path).map_err(|err| { + if err.kind() == io::ErrorKind::NotFound { + Error::MetadataNotFound { + path: meta_path.clone(), + version, + } + } else { + Error::IoPath { + path: path.to_path_buf(), + err, + } + } + }); + + async move { + let reader = reader?; + let reader: Box = Box::new(AllowStdIo::new(reader)); + Ok(reader) + } + .boxed() + } + + fn fetch_target_from_path( + &self, + target_path: &TargetPath, + path: &Path, + ) -> BoxFuture<'_, Result>> { + let reader = File::open(&path).map_err(|err| { + if err.kind() == io::ErrorKind::NotFound { + Error::TargetNotFound(target_path.clone()) + } else { + Error::IoPath { + path: path.to_path_buf(), + err, + } + } + }); + + async move { + let reader = reader?; + let reader: Box = Box::new(AllowStdIo::new(reader)); + Ok(reader) + } + .boxed() + } +} + +impl RepositoryProvider for FileSystemRepository +where + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + let path = self.metadata_path(meta_path, version); + self.fetch_metadata_from_path(meta_path, version, &path) + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + let path = self.target_path(target_path); + self.fetch_target_from_path(target_path, &path) + } +} + +impl RepositoryStorage for FileSystemRepository +where + D: DataInterchange + Sync + Send, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + let path = self.metadata_path(meta_path, version); + + async move { + if path.exists() { + debug!("Metadata path exists. Overwriting: {:?}", path); + } + + let mut temp_file = AllowStdIo::new(create_temp_file(&path)?); + if let Err(err) = copy(metadata, &mut temp_file).await { + return Err(Error::IoPath { path, err }); + } + temp_file + .into_inner() + .persist(&path) + .map_err(|err| Error::IoPath { + path, + err: err.error, + })?; + + Ok(()) + } + .boxed() + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + read: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + let path = self.target_path(target_path); + + async move { + if path.exists() { + debug!("Target path exists. Overwriting: {:?}", path); + } + + let mut temp_file = AllowStdIo::new(create_temp_file(&path)?); + if let Err(err) = copy(read, &mut temp_file).await { + return Err(Error::IoPath { path, err }); + } + temp_file + .into_inner() + .persist(&path) + .map_err(|err| Error::IoPath { + path, + err: err.error, + })?; + + Ok(()) + } + .boxed() + } +} + +/// [FileSystemBatchUpdate] is a special repository that is designed to write the metadata and +/// targets to an [FileSystemRepository] in a single batch. +/// +/// Note: `FileSystemBatchUpdate::commit()` must be called in order to write the metadata and +/// targets to the [FileSystemRepository]. Otherwise any queued changes will be lost on drop. +#[derive(Debug)] +pub struct FileSystemBatchUpdate<'a, D: DataInterchange> { + repo: &'a mut FileSystemRepository, + metadata: HashMap, + targets: HashMap, +} + +impl<'a, D> FileSystemBatchUpdate<'a, D> +where + D: DataInterchange + Sync, +{ + /// Write all the metadata and targets the [FileSystemBatchUpdate] to the source + /// [FileSystemRepository] in a single batch operation. + /// + /// Note: While this function will atomically write each file, it's possible that this could + /// fail with part of the files written if we experience a system error during the process. + pub async fn commit(self) -> Result<()> { + for (path, tmp_path) in self.targets { + if path.exists() { + debug!("Target path exists. Overwriting: {:?}", path); + } + tmp_path.persist(&path).map_err(|err| Error::IoPath { + path, + err: err.error, + })?; + } + + for (path, tmp_path) in self.metadata { + if path.exists() { + debug!("Metadata path exists. Overwriting: {:?}", path); + } + tmp_path.persist(path).map_err(|err| err.error)?; + } + + Ok(()) + } +} + +impl RepositoryProvider for FileSystemBatchUpdate<'_, D> +where + D: DataInterchange + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + let path = self.repo.metadata_path(meta_path, version); + if let Some(temp_path) = self.metadata.get(&path) { + self.repo + .fetch_metadata_from_path(meta_path, version, temp_path) + } else { + self.repo + .fetch_metadata_from_path(meta_path, version, &path) + } + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + let path = self.repo.target_path(target_path); + if let Some(temp_path) = self.targets.get(&path) { + self.repo.fetch_target_from_path(target_path, temp_path) + } else { + self.repo.fetch_target_from_path(target_path, &path) + } + } +} + +impl RepositoryStorage for FileSystemBatchUpdate<'_, D> +where + D: DataInterchange + Sync, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + read: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + let path = self.repo.metadata_path(meta_path, version); + let metadata = &mut self.metadata; + + async move { + let mut temp_file = AllowStdIo::new(create_temp_file(&path)?); + if let Err(err) = copy(read, &mut temp_file).await { + return Err(Error::IoPath { path, err }); + } + metadata.insert(path, temp_file.into_inner().into_temp_path()); + + Ok(()) + } + .boxed() + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + read: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + let path = self.repo.target_path(target_path); + let targets = &mut self.targets; + + async move { + let mut temp_file = AllowStdIo::new(create_temp_file(&path)?); + if let Err(err) = copy(read, &mut temp_file).await { + return Err(Error::IoPath { path, err }); + } + targets.insert(path, temp_file.into_inner().into_temp_path()); + + Ok(()) + } + .boxed() + } +} + +fn create_temp_file(path: &Path) -> Result { + // We want to atomically write the file to make sure clients can never see a partially written + // file. In order to do this, we'll write to a temporary file in the same directory as our + // target, otherwise we risk writing the temporary file to one mountpoint, and then + // non-atomically copying the file to another mountpoint. + + if let Some(parent) = path.parent() { + DirBuilder::new() + .recursive(true) + .create(parent) + .map_err(|err| Error::IoPath { + path: parent.to_path_buf(), + err, + })?; + Ok(NamedTempFile::new_in(parent).map_err(|err| Error::IoPath { + path: parent.to_path_buf(), + err, + })?) + } else { + Ok(NamedTempFile::new_in(".").map_err(|err| Error::IoPath { + path: path.to_path_buf(), + err, + })?) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::error::Error; + use crate::interchange::Json; + use crate::metadata::RootMetadata; + use crate::repository::{fetch_metadata_to_string, fetch_target_to_string, Repository}; + use assert_matches::assert_matches; + use futures_executor::block_on; + use futures_util::io::AsyncReadExt; + use tempfile; + + #[test] + fn file_system_repo_metadata_not_found_error() { + block_on(async { + let temp_dir = tempfile::Builder::new() + .prefix("rust-tuf") + .tempdir() + .unwrap(); + let repo = FileSystemRepositoryBuilder::new(temp_dir.path()) + .build() + .unwrap(); + + assert_matches!( + Repository::<_, Json>::new(repo) + .fetch_metadata::( + &MetadataPath::root(), + MetadataVersion::None, + None, + vec![], + ) + .await, + Err(Error::MetadataNotFound { + path, + version, + }) + if path == MetadataPath::root() && version == MetadataVersion::None + ); + }) + } + + #[test] + fn file_system_repo_targets() { + block_on(async { + let temp_dir = tempfile::Builder::new() + .prefix("rust-tuf") + .tempdir() + .unwrap(); + let mut repo = FileSystemRepositoryBuilder::::new(temp_dir.path().to_path_buf()) + .metadata_prefix("meta") + .targets_prefix("targs") + .build() + .unwrap(); + + // test that init worked + assert!(temp_dir.path().join("meta").exists()); + assert!(temp_dir.path().join("targs").exists()); + + let data: &[u8] = b"like tears in the rain"; + let path = TargetPath::new("foo/bar/baz").unwrap(); + repo.store_target(&path, &mut &*data).await.unwrap(); + assert!(temp_dir + .path() + .join("targs") + .join("foo") + .join("bar") + .join("baz") + .exists()); + + let mut buf = Vec::new(); + + // Enclose `fetch_target` in a scope to make sure the file is closed. + // This is needed for `tempfile` on Windows, which doesn't open the + // files in a mode that allows the file to be opened multiple times. + { + let mut read = repo.fetch_target(&path).await.unwrap(); + read.read_to_end(&mut buf).await.unwrap(); + assert_eq!(buf.as_slice(), data); + } + + // RepositoryProvider implementations do not guarantee data is not corrupt. + let bad_data: &[u8] = b"you're in a desert"; + repo.store_target(&path, &mut &*bad_data).await.unwrap(); + let mut read = repo.fetch_target(&path).await.unwrap(); + buf.clear(); + read.read_to_end(&mut buf).await.unwrap(); + assert_eq!(buf.as_slice(), bad_data); + }) + } + + #[test] + fn file_system_repo_batch_update() { + block_on(async { + let temp_dir = tempfile::Builder::new() + .prefix("rust-tuf") + .tempdir() + .unwrap(); + let mut repo = FileSystemRepositoryBuilder::::new(temp_dir.path().to_path_buf()) + .metadata_prefix("meta") + .targets_prefix("targs") + .build() + .unwrap(); + + let meta_path = MetadataPath::new("meta").unwrap(); + let meta_version = MetadataVersion::None; + let target_path = TargetPath::new("target").unwrap(); + + // First, write some stuff to the repository. + let committed_meta = "committed meta"; + let committed_target = "committed target"; + + repo.store_metadata(&meta_path, meta_version, &mut committed_meta.as_bytes()) + .await + .unwrap(); + + repo.store_target(&target_path, &mut committed_target.as_bytes()) + .await + .unwrap(); + + let mut batch = repo.batch_update(); + + // Make sure we can read back the committed stuff. + assert_eq!( + fetch_metadata_to_string(&batch, &meta_path, meta_version) + .await + .unwrap(), + committed_meta, + ); + assert_eq!( + fetch_target_to_string(&batch, &target_path).await.unwrap(), + committed_target, + ); + + // Next, stage some stuff in the batch_update. + let staged_meta = "staged meta"; + let staged_target = "staged target"; + batch + .store_metadata(&meta_path, meta_version, &mut staged_meta.as_bytes()) + .await + .unwrap(); + batch + .store_target(&target_path, &mut staged_target.as_bytes()) + .await + .unwrap(); + + // Make sure it got staged. + assert_eq!( + fetch_metadata_to_string(&batch, &meta_path, meta_version) + .await + .unwrap(), + staged_meta, + ); + assert_eq!( + fetch_target_to_string(&batch, &target_path).await.unwrap(), + staged_target, + ); + + // Next, drop the batch_update. We shouldn't have written the data back to the + // repository. + drop(batch); + + assert_eq!( + fetch_metadata_to_string(&repo, &meta_path, meta_version) + .await + .unwrap(), + committed_meta, + ); + assert_eq!( + fetch_target_to_string(&repo, &target_path).await.unwrap(), + committed_target, + ); + + // Do the batch_update again, but this time write the data. + let mut batch = repo.batch_update(); + batch + .store_metadata(&meta_path, meta_version, &mut staged_meta.as_bytes()) + .await + .unwrap(); + batch + .store_target(&target_path, &mut staged_target.as_bytes()) + .await + .unwrap(); + batch.commit().await.unwrap(); + + // Make sure the new data got to the repository. + assert_eq!( + fetch_metadata_to_string(&repo, &meta_path, meta_version) + .await + .unwrap(), + staged_meta, + ); + assert_eq!( + fetch_target_to_string(&repo, &target_path).await.unwrap(), + staged_target, + ); + }) + } +} diff --git a/vendor/tuf/src/repository/http.rs b/vendor/tuf/src/repository/http.rs new file mode 100644 index 0000000000..d4a53ecdcc --- /dev/null +++ b/vendor/tuf/src/repository/http.rs @@ -0,0 +1,459 @@ +//! Read-only Repository implementation backed by a web server. + +use futures_io::AsyncRead; +use futures_util::future::{BoxFuture, FutureExt}; +use futures_util::stream::TryStreamExt; +use http::{Response, StatusCode, Uri}; +use hyper::body::Body; +use hyper::client::connect::Connect; +use hyper::Client; +use hyper::Request; +use percent_encoding::utf8_percent_encode; +use std::io; +use std::marker::PhantomData; +use url::Url; + +use crate::error::Error; +use crate::interchange::DataInterchange; +use crate::metadata::{MetadataPath, MetadataVersion, TargetPath}; +use crate::repository::RepositoryProvider; +use crate::util::SafeAsyncRead; +use crate::Result; + +/// A builder to create a repository accessible over HTTP. +pub struct HttpRepositoryBuilder +where + C: Connect + Sync + 'static, + D: DataInterchange, +{ + uri: Uri, + client: Client, + user_agent: Option, + metadata_prefix: Option>, + targets_prefix: Option>, + min_bytes_per_second: u32, + _interchange: PhantomData, +} + +impl HttpRepositoryBuilder +where + C: Connect + Sync + 'static, + D: DataInterchange, +{ + /// Create a new repository with the given `Url` and `Client`. + pub fn new(url: Url, client: Client) -> Self { + HttpRepositoryBuilder { + uri: url.to_string().parse::().unwrap(), // This is dangerous, but will only exist for a short time as we migrate APIs. + client, + user_agent: None, + metadata_prefix: None, + targets_prefix: None, + min_bytes_per_second: 4096, + _interchange: PhantomData, + } + } + + /// Create a new repository with the given `Url` and `Client`. + pub fn new_with_uri(uri: Uri, client: Client) -> Self { + HttpRepositoryBuilder { + uri, + client, + user_agent: None, + metadata_prefix: None, + targets_prefix: None, + min_bytes_per_second: 4096, + _interchange: PhantomData, + } + } + + /// Set the User-Agent prefix. + /// + /// Callers *should* include a custom User-Agent prefix to help maintainers of TUF repositories + /// keep track of which client versions exist in the field. + /// + pub fn user_agent>(mut self, user_agent: T) -> Self { + self.user_agent = Some(user_agent.into()); + self + } + + /// The argument `metadata_prefix` is used to provide an alternate path where metadata is + /// stored on the repository. If `None`, this defaults to `/`. For example, if there is a TUF + /// repository at `https://tuf.example.com/`, but all metadata is stored at `/meta/`, then + /// passing the arg `Some("meta".into())` would cause `root.json` to be fetched from + /// `https://tuf.example.com/meta/root.json`. + pub fn metadata_prefix(mut self, metadata_prefix: Vec) -> Self { + self.metadata_prefix = Some(metadata_prefix); + self + } + + /// The argument `targets_prefix` is used to provide an alternate path where targets is + /// stored on the repository. If `None`, this defaults to `/`. For example, if there is a TUF + /// repository at `https://tuf.example.com/`, but all targets are stored at `/targets/`, then + /// passing the arg `Some("targets".into())` would cause `hello-world` to be fetched from + /// `https://tuf.example.com/targets/hello-world`. + pub fn targets_prefix(mut self, targets_prefix: Vec) -> Self { + self.targets_prefix = Some(targets_prefix); + self + } + + /// Set the minimum bytes per second for a read to be considered good. + pub fn min_bytes_per_second(mut self, min: u32) -> Self { + self.min_bytes_per_second = min; + self + } + + /// Build a `HttpRepository`. + pub fn build(self) -> HttpRepository { + let user_agent = match self.user_agent { + Some(user_agent) => user_agent, + None => "rust-tuf".into(), + }; + + HttpRepository { + uri: self.uri, + client: self.client, + user_agent, + metadata_prefix: self.metadata_prefix, + targets_prefix: self.targets_prefix, + min_bytes_per_second: self.min_bytes_per_second, + _interchange: PhantomData, + } + } +} + +/// A repository accessible over HTTP. +pub struct HttpRepository +where + C: Connect + Sync + 'static, + D: DataInterchange, +{ + uri: Uri, + client: Client, + user_agent: String, + metadata_prefix: Option>, + targets_prefix: Option>, + min_bytes_per_second: u32, + _interchange: PhantomData, +} + +// Configuration for urlencoding URI path elements. +// From https://url.spec.whatwg.org/#path-percent-encode-set +const URLENCODE_FRAGMENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS + .add(b' ') + .add(b'"') + .add(b'<') + .add(b'>') + .add(b'`'); +const URLENCODE_PATH: &percent_encoding::AsciiSet = + &URLENCODE_FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); + +fn extend_uri(uri: &Uri, prefix: &Option>, components: &[String]) -> Result { + let uri = uri.clone(); + let mut uri_parts = uri.into_parts(); + + let (path, query) = match &uri_parts.path_and_query { + Some(path_and_query) => (path_and_query.path(), path_and_query.query()), + None => ("", None), + }; + + let mut modified_path = path.to_owned(); + if modified_path.ends_with('/') { + modified_path.pop(); + } + + let mut path_split = modified_path + .split('/') + .map(String::from) + .collect::>(); + let mut new_path_elements: Vec<&str> = vec![]; + + if let Some(ref prefix) = prefix { + new_path_elements.extend(prefix.iter().map(String::as_str)); + } + new_path_elements.extend(components.iter().map(String::as_str)); + + // Urlencode new items to match behavior of PathSegmentsMut.extend from + // https://docs.rs/url/2.1.0/url/struct.PathSegmentsMut.html + let encoded_new_path_elements = new_path_elements + .into_iter() + .map(|path_segment| utf8_percent_encode(path_segment, URLENCODE_PATH).collect()); + path_split.extend(encoded_new_path_elements); + let constructed_path = path_split.join("/"); + + uri_parts.path_and_query = + match query { + Some(query) => Some(format!("{}?{}", constructed_path, query).parse().map_err( + |_| { + Error::IllegalArgument(format!( + "Invalid path and query: {:?}, {:?}", + constructed_path, query + )) + }, + )?), + None => Some(constructed_path.parse().map_err(|_| { + Error::IllegalArgument(format!("Invalid URI path: {:?}", constructed_path)) + })?), + }; + + Uri::from_parts(uri_parts).map_err(|_| { + Error::IllegalArgument(format!( + "Invalid URI parts: {:?}, {:?}, {:?}", + constructed_path, prefix, components + )) + }) +} + +impl HttpRepository +where + C: Connect + Clone + Send + Sync + 'static, + D: DataInterchange, +{ + async fn get<'a>(&'a self, uri: &Uri) -> Result> { + match Request::builder() + .uri(uri) + .header("User-Agent", &*self.user_agent) + .body(Body::default()) + { + Ok(req) => match self.client.request(req).await { + Ok(resp) => Ok(resp), + Err(err) => Err(Error::Hyper { + uri: uri.to_string(), + err, + }), + }, + Err(err) => Err(Error::Http { + uri: uri.to_string(), + err, + }), + } + } +} + +impl RepositoryProvider for HttpRepository +where + C: Connect + Clone + Send + Sync + 'static, + D: DataInterchange + Send + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + let meta_path = meta_path.clone(); + let components = meta_path.components::(version); + let uri = extend_uri(&self.uri, &self.metadata_prefix, &components); + + async move { + // TODO(#278) check content length if known and fail early if the payload is too large. + + let uri = uri?; + let resp = self.get(&uri).await?; + + let status = resp.status(); + if status == StatusCode::OK { + let reader = resp + .into_body() + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + .into_async_read() + .enforce_minimum_bitrate(self.min_bytes_per_second); + + let reader: Box = Box::new(reader); + Ok(reader) + } else if status == StatusCode::NOT_FOUND { + Err(Error::MetadataNotFound { + path: meta_path, + version, + }) + } else { + Err(Error::BadHttpStatus { + uri: uri.to_string(), + code: status, + }) + } + } + .boxed() + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + let target_path = target_path.clone(); + let components = target_path.components(); + let uri = extend_uri(&self.uri, &self.targets_prefix, &components); + + async move { + // TODO(#278) check content length if known and fail early if the payload is too large. + + let uri = uri?; + let resp = self.get(&uri).await?; + + let status = resp.status(); + if status == StatusCode::OK { + let reader = resp + .into_body() + .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + .into_async_read() + .enforce_minimum_bitrate(self.min_bytes_per_second); + + let reader: Box = Box::new(reader); + Ok(reader) + } else if status == StatusCode::NOT_FOUND { + Err(Error::TargetNotFound(target_path)) + } else { + Err(Error::BadHttpStatus { + uri: uri.to_string(), + code: status, + }) + } + } + .boxed() + } +} + +#[cfg(test)] +mod test { + use super::*; + + // Old behavior of the `HttpRepository::get` extension + // functionality + fn http_repository_extend_using_url( + base_url: Url, + prefix: &Option>, + components: &[String], + ) -> url::Url { + let mut url = base_url; + { + let mut segments = url.path_segments_mut().unwrap(); + if let Some(ref prefix) = prefix { + segments.extend(prefix); + } + segments.extend(components); + } + url + } + + #[test] + fn http_repository_uri_construction() { + let base_uri = "http://example.com/one"; + + let prefix = Some(vec![String::from("prefix")]); + let components = [ + String::from("components_one"), + String::from("components_two"), + ]; + + let uri = base_uri.parse::().unwrap(); + let extended_uri = extend_uri(&uri, &prefix, &components).unwrap(); + + let url = + http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components); + + assert_eq!(url.to_string(), extended_uri.to_string()); + assert_eq!( + extended_uri.to_string(), + "http://example.com/one/prefix/components_one/components_two" + ); + } + + #[test] + fn http_repository_uri_construction_encoded() { + let base_uri = "http://example.com/one"; + + let prefix = Some(vec![String::from("prefix")]); + let components = [String::from("chars to encode#?")]; + let uri = base_uri.parse::().unwrap(); + let extended_uri = extend_uri(&uri, &prefix, &components) + .expect("correctly generated a URI with a zone id"); + + let url = + http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components); + + assert_eq!(url.to_string(), extended_uri.to_string()); + assert_eq!( + extended_uri.to_string(), + "http://example.com/one/prefix/chars%20to%20encode%23%3F" + ); + } + + #[test] + fn http_repository_uri_construction_no_components() { + let base_uri = "http://example.com/one"; + + let prefix = Some(vec![String::from("prefix")]); + let components = []; + + let uri = base_uri.parse::().unwrap(); + let extended_uri = extend_uri(&uri, &prefix, &components).unwrap(); + + let url = + http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components); + + assert_eq!(url.to_string(), extended_uri.to_string()); + assert_eq!(extended_uri.to_string(), "http://example.com/one/prefix"); + } + + #[test] + fn http_repository_uri_construction_no_prefix() { + let base_uri = "http://example.com/one"; + + let prefix = None; + let components = [ + String::from("components_one"), + String::from("components_two"), + ]; + + let uri = base_uri.parse::().unwrap(); + let extended_uri = extend_uri(&uri, &prefix, &components).unwrap(); + + let url = + http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components); + + assert_eq!(url.to_string(), extended_uri.to_string()); + assert_eq!( + extended_uri.to_string(), + "http://example.com/one/components_one/components_two" + ); + } + + #[test] + fn http_repository_uri_construction_with_query() { + let base_uri = "http://example.com/one?test=1"; + + let prefix = None; + let components = [ + String::from("components_one"), + String::from("components_two"), + ]; + + let uri = base_uri.parse::().unwrap(); + let extended_uri = extend_uri(&uri, &prefix, &components).unwrap(); + + let url = + http_repository_extend_using_url(Url::parse(base_uri).unwrap(), &prefix, &components); + + assert_eq!(url.to_string(), extended_uri.to_string()); + assert_eq!( + extended_uri.to_string(), + "http://example.com/one/components_one/components_two?test=1" + ); + } + + #[test] + fn http_repository_uri_construction_ipv6_zoneid() { + let base_uri = "http://[aaaa::aaaa:aaaa:aaaa:1234%252]:80"; + + let prefix = Some(vec![String::from("prefix")]); + let components = [ + String::from("componenents_one"), + String::from("components_two"), + ]; + let uri = base_uri.parse::().unwrap(); + let extended_uri = extend_uri(&uri, &prefix, &components) + .expect("correctly generated a URI with a zone id"); + assert_eq!( + extended_uri.to_string(), + "http://[aaaa::aaaa:aaaa:aaaa:1234%252]:80/prefix/componenents_one/components_two" + ); + } +} diff --git a/vendor/tuf/src/repository/track_repo.rs b/vendor/tuf/src/repository/track_repo.rs new file mode 100644 index 0000000000..c7d076224e --- /dev/null +++ b/vendor/tuf/src/repository/track_repo.rs @@ -0,0 +1,190 @@ +use { + crate::{ + interchange::DataInterchange, + metadata::{Metadata, MetadataPath, MetadataVersion, RawSignedMetadata, TargetPath}, + repository::{RepositoryProvider, RepositoryStorage}, + Result, + }, + futures_io::AsyncRead, + futures_util::{ + future::{BoxFuture, FutureExt}, + io::{AsyncReadExt, Cursor}, + }, + std::sync::{Arc, Mutex}, +}; + +#[derive(Debug, PartialEq)] +pub(crate) enum Track { + Store { + path: MetadataPath, + version: MetadataVersion, + metadata: String, + }, + FetchFound { + path: MetadataPath, + version: MetadataVersion, + metadata: String, + }, + FetchErr(MetadataPath, MetadataVersion), +} + +impl Track { + pub(crate) fn store(meta_path: &MetadataPath, version: MetadataVersion, metadata: T) -> Self + where + T: Into>, + { + Track::Store { + path: meta_path.clone(), + version, + metadata: String::from_utf8(metadata.into()).unwrap(), + } + } + + pub(crate) fn store_meta( + version: MetadataVersion, + metadata: &RawSignedMetadata, + ) -> Self + where + M: Metadata, + D: DataInterchange, + { + Self::store(&M::ROLE.into(), version, metadata.as_bytes()) + } + + pub(crate) fn fetch_found( + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: T, + ) -> Self + where + T: Into>, + { + Track::FetchFound { + path: meta_path.clone(), + version, + metadata: String::from_utf8(metadata.into()).unwrap(), + } + } + + pub(crate) fn fetch_meta_found( + version: MetadataVersion, + metadata: &RawSignedMetadata, + ) -> Self + where + M: Metadata, + D: DataInterchange, + { + Track::fetch_found(&M::ROLE.into(), version, metadata.as_bytes()) + } +} + +/// Helper Repository wrapper that tracks all the metadata fetches and stores for testing purposes. +pub(crate) struct TrackRepository { + repo: R, + tracks: Arc>>, +} + +impl TrackRepository { + pub(crate) fn new(repo: R) -> Self { + Self { + repo, + tracks: Arc::new(Mutex::new(vec![])), + } + } + + pub(crate) fn take_tracks(&self) -> Vec { + self.tracks.lock().unwrap().drain(..).collect() + } + + pub(crate) fn as_inner_mut(&mut self) -> &mut R { + &mut self.repo + } +} + +impl RepositoryStorage for TrackRepository +where + R: RepositoryStorage + Sync + Send, + D: DataInterchange + Sync, +{ + fn store_metadata<'a>( + &'a mut self, + meta_path: &MetadataPath, + version: MetadataVersion, + metadata: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + let meta_path = meta_path.clone(); + async move { + let mut buf = Vec::new(); + metadata.read_to_end(&mut buf).await?; + + let () = self + .repo + .store_metadata(&meta_path, version, &mut buf.as_slice()) + .await?; + + self.tracks + .lock() + .unwrap() + .push(Track::store(&meta_path, version, buf)); + + Ok(()) + } + .boxed() + } + + fn store_target<'a>( + &'a mut self, + target_path: &TargetPath, + target: &'a mut (dyn AsyncRead + Send + Unpin + 'a), + ) -> BoxFuture<'a, Result<()>> { + self.repo.store_target(target_path, target) + } +} + +impl RepositoryProvider for TrackRepository +where + D: DataInterchange + Sync, + R: RepositoryProvider + Sync, +{ + fn fetch_metadata<'a>( + &'a self, + meta_path: &MetadataPath, + version: MetadataVersion, + ) -> BoxFuture<'a, Result>> { + let meta_path = meta_path.clone(); + async move { + let fut = self.repo.fetch_metadata(&meta_path, version); + match fut.await { + Ok(mut rdr) => { + let mut buf = Vec::new(); + rdr.read_to_end(&mut buf).await?; + + self.tracks.lock().unwrap().push(Track::fetch_found( + &meta_path, + version, + buf.clone(), + )); + + let rdr: Box = Box::new(Cursor::new(buf)); + + Ok(rdr) + } + Err(err) => { + self.tracks + .lock() + .unwrap() + .push(Track::FetchErr(meta_path, version)); + Err(err) + } + } + } + .boxed() + } + + fn fetch_target<'a>( + &'a self, + target_path: &TargetPath, + ) -> BoxFuture<'a, Result>> { + self.repo.fetch_target(target_path) + } +} diff --git a/vendor/tuf/src/util.rs b/vendor/tuf/src/util.rs new file mode 100644 index 0000000000..2683349714 --- /dev/null +++ b/vendor/tuf/src/util.rs @@ -0,0 +1,345 @@ +use futures_io::AsyncRead; +use futures_util::ready; +use ring::digest; +use std::io::{self, ErrorKind}; +use std::marker::Unpin; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::{Duration, Instant}; + +use crate::crypto::{HashAlgorithm, HashValue}; +use crate::Result; + +pub(crate) trait SafeAsyncRead: AsyncRead + Sized + Unpin { + /// Creates an `AsyncRead` adapter which will fail transfers slower than + /// `min_bytes_per_second`. + fn enforce_minimum_bitrate(self, min_bytes_per_second: u32) -> EnforceMinimumBitrate { + EnforceMinimumBitrate::new(self, min_bytes_per_second) + } + + /// Creates an `AsyncRead` adapter that ensures the consumer can't read more than `max_length` + /// bytes. Also, when the underlying `AsyncRead` is fully consumed, the hash of the data is + /// optionally calculated and checked against `hash_data`. Consumers should purge and untrust + /// all read bytes if the returned `AsyncRead` ever returns an `Err`. + /// + /// It is **critical** that none of the bytes from this struct are used until it has been fully + /// consumed as the data is untrusted. + fn check_length_and_hash( + self, + max_length: u64, + hash_data: Vec<(&'static HashAlgorithm, HashValue)>, + ) -> Result> { + SafeReader::new(self, max_length, hash_data) + } +} + +impl SafeAsyncRead for R {} + +/// Wraps an `AsyncRead` to detect and fail transfers slower than a minimum bitrate. +pub(crate) struct EnforceMinimumBitrate { + inner: R, + min_bytes_per_second: u32, + start_time: Option, + bytes_read: u64, +} + +impl EnforceMinimumBitrate { + /// Create a new `EnforceMinimumBitrate`. + pub(crate) fn new(read: R, min_bytes_per_second: u32) -> Self { + Self { + inner: read, + min_bytes_per_second, + start_time: None, + bytes_read: 0, + } + } +} + +#[cfg(not(test))] +const BITRATE_GRACE_PERIOD: Duration = Duration::from_secs(30); +#[cfg(test)] +const BITRATE_GRACE_PERIOD: Duration = Duration::from_secs(1); + +impl AsyncRead for EnforceMinimumBitrate { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll> { + // FIXME(#272) transfers that stall out completely won't enforce the minimum bit rate. + let read_bytes = ready!(Pin::new(&mut self.inner).poll_read(cx, buf))?; + + let start_time = *self.start_time.get_or_insert_with(Instant::now); + + if read_bytes == 0 { + return Poll::Ready(Ok(0)); + } + + self.bytes_read += read_bytes as u64; + + // allow a grace period before we start checking the bitrate + let duration = start_time.elapsed(); + if duration >= BITRATE_GRACE_PERIOD { + if (self.bytes_read as f32) / duration.as_secs_f32() < self.min_bytes_per_second as f32 + { + return Poll::Ready(Err(io::Error::new( + ErrorKind::TimedOut, + "Read aborted. Bitrate too low.", + ))); + } + } + + Poll::Ready(Ok(read_bytes)) + } +} + +/// Wrapper to verify a byte stream as it is read. +/// +/// Wraps an `AsyncRead` to ensure that the consumer can't read more than a capped maximum number of +/// bytes. Also, when the underlying `AsyncRead` is fully consumed, the hash of the data is +/// optionally calculated. If the calculated hash does not match the given hash, it will return an +/// `Err`. Consumers of a `SafeReader` should purge and untrust all read bytes if this ever returns +/// an `Err`. +/// +/// It is **critical** that none of the bytes from this struct are used until it has been fully +/// consumed as the data is untrusted. +pub(crate) struct SafeReader { + inner: R, + max_size: u64, + hashers: Vec<(digest::Context, HashValue)>, + bytes_read: u64, +} + +impl SafeReader { + /// Create a new `SafeReader`. + /// + /// The argument `hash_data` takes a `HashAlgorithm` and expected `HashValue`. The given + /// algorithm is used to hash the data as it is read. At the end of the stream, the digest is + /// calculated and compared against `HashValue`. If the two are not equal, it means the data + /// stream has been corrupted or tampered with in some way. + pub(crate) fn new( + read: R, + max_size: u64, + hash_data: Vec<(&'static HashAlgorithm, HashValue)>, + ) -> Result { + let mut hashers = Vec::with_capacity(hash_data.len()); + for (alg, value) in hash_data { + hashers.push((alg.digest_context()?, value)); + } + + Ok(SafeReader { + inner: read, + max_size, + hashers, + bytes_read: 0, + }) + } +} + +impl AsyncRead for SafeReader { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut [u8], + ) -> Poll> { + let read_bytes = ready!(Pin::new(&mut self.inner).poll_read(cx, buf))?; + + if read_bytes == 0 { + for (context, expected_hash) in self.hashers.drain(..) { + let generated_hash = context.finish(); + if generated_hash.as_ref() != expected_hash.value() { + return Poll::Ready(Err(io::Error::new( + ErrorKind::InvalidData, + "Calculated hash did not match the required hash.", + ))); + } + } + + return Poll::Ready(Ok(0)); + } + + match self.bytes_read.checked_add(read_bytes as u64) { + Some(sum) if sum <= self.max_size => self.bytes_read = sum, + _ => { + return Poll::Ready(Err(io::Error::new( + ErrorKind::InvalidData, + "Read exceeded the maximum allowed bytes.", + ))); + } + } + + for (ref mut context, _) in &mut self.hashers { + context.update(&buf[..read_bytes]); + } + + Poll::Ready(Ok(read_bytes)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures_executor::block_on; + use futures_util::io::AsyncReadExt; + use ring::digest::SHA256; + + #[test] + fn valid_read() { + block_on(async { + let bytes: &[u8] = &[0x00, 0x01, 0x02, 0x03]; + let mut reader = SafeReader::new(bytes, bytes.len() as u64, vec![]).unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_ok()); + assert_eq!(buf, bytes); + }) + } + + #[test] + fn valid_read_large_data() { + block_on(async { + let bytes: &[u8] = &[0x00; 64 * 1024]; + let mut reader = SafeReader::new(bytes, bytes.len() as u64, vec![]).unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_ok()); + assert_eq!(buf, bytes); + }) + } + + #[test] + fn valid_read_below_max_size() { + block_on(async { + let bytes: &[u8] = &[0x00, 0x01, 0x02, 0x03]; + let mut reader = SafeReader::new(bytes, (bytes.len() as u64) + 1, vec![]).unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_ok()); + assert_eq!(buf, bytes); + }) + } + + #[test] + fn invalid_read_above_max_size() { + block_on(async { + let bytes: &[u8] = &[0x00, 0x01, 0x02, 0x03]; + let mut reader = SafeReader::new(bytes, (bytes.len() as u64) - 1, vec![]).unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_err()); + }) + } + + #[test] + fn invalid_read_above_max_size_large_data() { + block_on(async { + let bytes: &[u8] = &[0x00; 64 * 1024]; + let mut reader = SafeReader::new(bytes, (bytes.len() as u64) - 1, vec![]).unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_err()); + }) + } + + #[test] + fn valid_read_good_hash() { + block_on(async { + let bytes: &[u8] = &[0x00, 0x01, 0x02, 0x03]; + let mut context = digest::Context::new(&SHA256); + context.update(bytes); + let hash_value = HashValue::new(context.finish().as_ref().to_vec()); + let mut reader = SafeReader::new( + bytes, + bytes.len() as u64, + vec![(&HashAlgorithm::Sha256, hash_value)], + ) + .unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_ok()); + assert_eq!(buf, bytes); + }) + } + + #[test] + fn invalid_read_bad_hash() { + block_on(async { + let bytes: &[u8] = &[0x00, 0x01, 0x02, 0x03]; + let mut context = digest::Context::new(&SHA256); + context.update(bytes); + context.update(&[0xFF]); // evil bytes + let hash_value = HashValue::new(context.finish().as_ref().to_vec()); + let mut reader = SafeReader::new( + bytes, + bytes.len() as u64, + vec![(&HashAlgorithm::Sha256, hash_value)], + ) + .unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_err()); + }) + } + + #[test] + fn valid_read_good_hash_large_data() { + block_on(async { + let bytes: &[u8] = &[0x00; 64 * 1024]; + let mut context = digest::Context::new(&SHA256); + context.update(bytes); + let hash_value = HashValue::new(context.finish().as_ref().to_vec()); + let mut reader = SafeReader::new( + bytes, + bytes.len() as u64, + vec![(&HashAlgorithm::Sha256, hash_value)], + ) + .unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_ok()); + assert_eq!(buf, bytes); + }) + } + + #[test] + fn invalid_read_bad_hash_large_data() { + block_on(async { + let bytes: &[u8] = &[0x00; 64 * 1024]; + let mut context = digest::Context::new(&SHA256); + context.update(bytes); + context.update(&[0xFF]); // evil bytes + let hash_value = HashValue::new(context.finish().as_ref().to_vec()); + let mut reader = SafeReader::new( + bytes, + bytes.len() as u64, + vec![(&HashAlgorithm::Sha256, hash_value)], + ) + .unwrap(); + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_err()); + }) + } + + #[test] + fn enforce_minimum_bitrate_is_identity_for_fast_transfers() { + block_on(async { + let bytes: &[u8] = &[0x42; 64 * 1024]; + + let mut reader = EnforceMinimumBitrate::new(bytes, 100); + + let mut buf = Vec::new(); + assert!(reader.read_to_end(&mut buf).await.is_ok()); + assert_eq!(bytes, &buf[..]); + }) + } + + #[test] + fn enforce_minimum_bitrate_is_fails_when_reader_is_too_slow() { + block_on(async { + let bytes: &[u8] = &[0x42; 64 * 1024]; + + let mut reader = EnforceMinimumBitrate::new(bytes, 100); + + let mut buf = vec![0; 50]; + + assert!(reader.read_exact(&mut buf).await.is_ok()); + assert_eq!(buf, &[0x42; 50][..]); + + std::thread::sleep(BITRATE_GRACE_PERIOD); + + assert!(reader.read_to_end(&mut buf).await.is_err()); + }) + } +} diff --git a/vendor/tuf/src/verify.rs b/vendor/tuf/src/verify.rs new file mode 100644 index 0000000000..366b6fe64e --- /dev/null +++ b/vendor/tuf/src/verify.rs @@ -0,0 +1,165 @@ +//! The `verify` module performs signature verification. + +use log::{debug, warn}; +use serde_derive::Deserialize; +use std::collections::HashMap; + +use crate::crypto::{KeyId, PublicKey, Signature}; +use crate::error::Error; +use crate::interchange::DataInterchange; +use crate::metadata::{Metadata, MetadataPath, RawSignedMetadata}; + +/// `Verified` is a wrapper type that signifies the inner type has had it's signature verified. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Verified { + value: T, +} + +impl Verified { + // Create a new `Verified` around some type. This must be kept private to this module in order + // to guarantee the `V` can only be created through signature verification. + fn new(value: T) -> Self { + Verified { value } + } +} + +impl std::ops::Deref for Verified { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +/// Verify this metadata. +/// +/// ``` +/// # use chrono::prelude::*; +/// # use tuf::crypto::{Ed25519PrivateKey, PrivateKey, SignatureScheme, HashAlgorithm}; +/// # use tuf::interchange::Json; +/// # use tuf::metadata::{MetadataPath, SnapshotMetadataBuilder, SignedMetadata}; +/// # use tuf::verify::verify_signatures; +/// +/// let key_1: &[u8] = include_bytes!("../tests/ed25519/ed25519-1.pk8.der"); +/// let key_1 = Ed25519PrivateKey::from_pkcs8(&key_1).unwrap(); +/// +/// let key_2: &[u8] = include_bytes!("../tests/ed25519/ed25519-2.pk8.der"); +/// let key_2 = Ed25519PrivateKey::from_pkcs8(&key_2).unwrap(); +/// +/// let raw_snapshot = SnapshotMetadataBuilder::new() +/// .signed::(&key_1) +/// .unwrap() +/// .to_raw() +/// .unwrap(); +/// +/// assert!(verify_signatures( +/// &MetadataPath::snapshot(), +/// &raw_snapshot, +/// 1, +/// vec![key_1.public()], +/// ).is_ok()); +/// +/// // fail with increased threshold +/// assert!(verify_signatures( +/// &MetadataPath::snapshot(), +/// &raw_snapshot, +/// 2, +/// vec![key_1.public()], +/// ).is_err()); +/// +/// // fail when the keys aren't authorized +/// assert!(verify_signatures( +/// &MetadataPath::snapshot(), +/// &raw_snapshot, +/// 1, +/// vec![key_2.public()], +/// ).is_err()); +/// +/// // fail when the keys don't exist +/// assert!(verify_signatures( +/// &MetadataPath::snapshot(), +/// &raw_snapshot, +/// 1, +/// &[], +/// ).is_err()); +pub fn verify_signatures<'a, D, M, I>( + role: &MetadataPath, + raw_metadata: &RawSignedMetadata, + threshold: u32, + authorized_keys: I, +) -> Result, Error> +where + D: DataInterchange, + M: Metadata, + I: IntoIterator, +{ + if threshold < 1 { + return Err(Error::MetadataThresholdMustBeGreaterThanZero(role.clone())); + } + + let authorized_keys = authorized_keys + .into_iter() + .map(|k| (k.key_id(), k)) + .collect::>(); + + // Keep the parsed `signed` value so we can deserialize from it below without re-parsing + // the canonical bytes (which aren't strict JSON: OLPC canonical form leaves control chars + // literal). + let (signatures, canonical_bytes, signed_value) = { + #[derive(Deserialize)] + pub struct SignedMetadata { + signatures: Vec, + signed: D::RawData, + } + + let unverified: SignedMetadata = D::from_slice(raw_metadata.as_bytes())?; + + let canonical_bytes = D::canonicalize(&unverified.signed)?; + (unverified.signatures, canonical_bytes, unverified.signed) + }; + + let mut signatures_needed = threshold; + + // Create a key_id->signature map to deduplicate the key_ids. + let signatures = signatures + .iter() + .map(|sig| (sig.key_id(), sig)) + .collect::>(); + + for (key_id, sig) in signatures { + match authorized_keys.get(key_id) { + Some(pub_key) => match pub_key.verify(role, &canonical_bytes, sig) { + Ok(()) => { + debug!("Good signature from key ID {:?}", pub_key.key_id()); + signatures_needed -= 1; + } + Err(e) => { + warn!("Bad signature from key ID {:?}: {:?}", pub_key.key_id(), e); + } + }, + None => { + warn!( + "Key ID {:?} was not found in the set of authorized keys.", + sig.key_id() + ); + } + } + if signatures_needed == 0 { + break; + } + } + + if signatures_needed > 0 { + return Err(Error::MetadataMissingSignatures { + role: role.clone(), + number_of_valid_signatures: threshold - signatures_needed, + threshold, + }); + } + + // Deserialize from the already-parsed `signed` value; the canonical bytes (which the + // signatures cover) are intentionally not strict JSON, so `from_slice` on them would fail. + let verified_metadata = D::deserialize(&signed_value)?; + + Ok(Verified::new(verified_metadata)) +} diff --git a/vendor/tuf/tests/ecdsa/ecdsa_root.canonical b/vendor/tuf/tests/ecdsa/ecdsa_root.canonical new file mode 100644 index 0000000000..32df6723cd --- /dev/null +++ b/vendor/tuf/tests/ecdsa/ecdsa_root.canonical @@ -0,0 +1,9 @@ +{"_type":"root","consistent_snapshot":true,"expires":"2099-01-01T00:00:00Z","keys":{"bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d":{"keyid_hash_algorithms":["sha256","sha512"],"keytype":"ecdsa","keyval":{"public":"-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgqM1Wy2mqv8jaK43314CglNrw2dO +PFFdBVgNjMto5Us0Rny5c7dlRpYHMGm8T6cAkUEZpuRiFyGsdtQEKceeKg== +-----END PUBLIC KEY----- +"},"scheme":"ecdsa-sha2-nistp256"},"e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2":{"keyid_hash_algorithms":["sha256","sha512"],"keytype":"ecdsa","keyval":{"public":"-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkVu9OBGGly9YhA5MDdyJODThPkFZ ++7QOJnjYjQ14NuSQCTB3iPth8k7r5BXj6cvfoqOoeFU1mGfDL7wXGorETw== +-----END PUBLIC KEY----- +"},"scheme":"ecdsa-sha2-nistp256"}},"roles":{"root":{"keyids":["e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2","bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d"],"threshold":2},"snapshot":{"keyids":["e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2","bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d"],"threshold":2},"targets":{"keyids":["e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2","bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d"],"threshold":2},"timestamp":{"keyids":["e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2","bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d"],"threshold":2}},"spec_version":"1.0","version":1} \ No newline at end of file diff --git a/vendor/tuf/tests/ecdsa/ecdsa_root.json b/vendor/tuf/tests/ecdsa/ecdsa_root.json new file mode 100644 index 0000000000..caff6699d1 --- /dev/null +++ b/vendor/tuf/tests/ecdsa/ecdsa_root.json @@ -0,0 +1 @@ +{"signed": {"_type": "root", "spec_version": "1.0", "version": 1, "expires": "2099-01-01T00:00:00Z", "keys": {"e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2": {"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkVu9OBGGly9YhA5MDdyJODThPkFZ\n+7QOJnjYjQ14NuSQCTB3iPth8k7r5BXj6cvfoqOoeFU1mGfDL7wXGorETw==\n-----END PUBLIC KEY-----\n"}}, "bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d": {"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgqM1Wy2mqv8jaK43314CglNrw2dO\nPFFdBVgNjMto5Us0Rny5c7dlRpYHMGm8T6cAkUEZpuRiFyGsdtQEKceeKg==\n-----END PUBLIC KEY-----\n"}}}, "roles": {"root": {"keyids": ["e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2", "bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d"], "threshold": 2}, "snapshot": {"keyids": ["e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2", "bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d"], "threshold": 2}, "targets": {"keyids": ["e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2", "bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d"], "threshold": 2}, "timestamp": {"keyids": ["e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2", "bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d"], "threshold": 2}}, "consistent_snapshot": true}, "signatures": [{"keyid": "e58379d2785859f8df7e47273c331880a81aae0af7a2ec382a276593123aede2", "sig": "3045022100bebdf68c3f56a6c0489c40bc8a4a39d59433602aaa0f256b3d73b8078e8de36d02204675ce35a8f9c8b28fe312a2d91031843e42c53ee533974e1f5f4461a02d65da"}, {"keyid": "bf650e59197a14d12e521a1d7657f7d52fa01e1b3539aa01bab7c99d4278791d", "sig": "3045022100d3f1df2ad5b8d42a3bd927f23301106a988e88371e2923a4517a0ce211b826ae0220731cbfaa87237b687e55db75c0048f14101844f8af414c5a30448bdb73515fb3"}]} \ No newline at end of file diff --git a/vendor/tuf/tests/ed25519/ed25519-1 b/vendor/tuf/tests/ed25519/ed25519-1 new file mode 100644 index 0000000000000000000000000000000000000000..61f135fbbf93c656653c4d1e2beaac37b975c2d4 GIT binary patch literal 64 zcmV-G0Kfl=yN^7LK;KOkY>J0WPJo&YKG$g7mVzxx3ClSANJ*gUio$DLp71B0JLBjg WVyQdM7XDc7LL~sxjITx^&b5I literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/ed25519/ed25519-1.pk8.der b/vendor/tuf/tests/ed25519/ed25519-1.pk8.der new file mode 100644 index 0000000000000000000000000000000000000000..ea5dda63b0ef354f49911e2f02f933d55ddefbfd GIT binary patch literal 85 zcmV-b0IL5mQvv}2Fa-t!D`jv5A_O3cyN^7LK;KOkY>J0WPJo&YKG$g7mVzxx3ClSA rNJ*ffBLg7->x#l_T%PbJo;%~{B4VjK&KCYy?m{I1(~Pf19A%@n>6;<+ literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/ed25519/ed25519-1.pub b/vendor/tuf/tests/ed25519/ed25519-1.pub new file mode 100644 index 0000000000000000000000000000000000000000..e60ca2f5ccd98beec9a2d1eb7eee87ef7c24d5e1 GIT binary patch literal 32 qcmV+*0N?-Xio$DLp71B0JLBjgVyQdM7XDc7LL~sxjITx!lV&Ajfy=ybAb>L rZGaJ>BLg7-XxZ$uhc^%fsOyek5K{%iLsqE#*jd;9^UNZP^0KqCCn6+D literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/ed25519/ed25519-3.pk8.der b/vendor/tuf/tests/ed25519/ed25519-3.pk8.der new file mode 100644 index 0000000000000000000000000000000000000000..1b4cbedf4c968fc8fb1ea634eeff2b6fa06c13b5 GIT binary patch literal 85 zcmV-b0IL5mQvv}2Fa-t!D`jv5A_O4J>iGq95Z8<;$pK#L_$LC3g#v3alTogJ;K2}d rUicBABLg7-7GrkF3V(5Z0&rF*FE$({FR)O)(V%DlAc>$bFS@rqy*nQn literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/ed25519/ed25519-4.pk8.der b/vendor/tuf/tests/ed25519/ed25519-4.pk8.der new file mode 100644 index 0000000000000000000000000000000000000000..3e458e734999630b353e5f6e71cdbfe1a719717b GIT binary patch literal 85 zcmV-b0IL5mQvv}2Fa-t!D`jv5A_O2I1lcw7I+&}YPa(2E#u#OuUGDKZdhV?HSS6-v r+PJNuBLg7-6cDa3Q>t*g`j|qBhn-~804g|m%VE8{d2<6 r@Gj?}BLg7-Fe4!r{1g%+A96%>xqqk))*p%5DlA%::from_trusted_metadata(&metadata).unwrap(); + + //// build the targets //// + //// build the delegation //// + let target_file: &[u8] = b"bar"; + let delegation = TargetsMetadataBuilder::new() + .insert_target_from_slice( + TargetPath::new("foo").unwrap(), + target_file, + &[HashAlgorithm::Sha256], + ) + .unwrap() + .signed::(&delegation_key) + .unwrap(); + let raw_delegation = delegation.to_raw().unwrap(); + + tuf.update_delegated_targets( + &now, + &MetadataPath::targets(), + &MetadataPath::new("delegation").unwrap(), + &raw_delegation, + ) + .unwrap(); + + assert!(tuf + .target_description(&TargetPath::new("foo").unwrap()) + .is_ok()); + }) +} + +#[test] +fn nested_delegation() { + block_on(async { + let now = Utc::now(); + + let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let snapshot_key = Ed25519PrivateKey::from_pkcs8(ED25519_2_PK8).unwrap(); + let targets_key = Ed25519PrivateKey::from_pkcs8(ED25519_3_PK8).unwrap(); + let timestamp_key = Ed25519PrivateKey::from_pkcs8(ED25519_4_PK8).unwrap(); + let delegation_a_key = Ed25519PrivateKey::from_pkcs8(ED25519_5_PK8).unwrap(); + let delegation_b_key = Ed25519PrivateKey::from_pkcs8(ED25519_6_PK8).unwrap(); + + let mut repo = EphemeralRepository::new(); + let metadata = RepoBuilder::create(&mut repo) + .trusted_root_keys(&[&root_key]) + .trusted_snapshot_keys(&[&snapshot_key]) + .trusted_targets_keys(&[&targets_key]) + .trusted_timestamp_keys(&[×tamp_key]) + .stage_root() + .unwrap() + .add_delegation_key(delegation_a_key.public().clone()) + .add_delegation_role( + Delegation::builder(MetadataPath::new("delegation-a").unwrap()) + .key(delegation_a_key.public()) + .delegate_path(TargetPath::new("foo").unwrap()) + .build() + .unwrap(), + ) + .stage_targets() + .unwrap() + .stage_snapshot_with_builder(|builder| { + builder + .insert_metadata_description( + MetadataPath::new("delegation-a").unwrap(), + MetadataDescription::from_slice(&[0u8], 1, &[HashAlgorithm::Sha256]) + .unwrap(), + ) + .insert_metadata_description( + MetadataPath::new("delegation-b").unwrap(), + MetadataDescription::from_slice(&[0u8], 1, &[HashAlgorithm::Sha256]) + .unwrap(), + ) + }) + .unwrap() + .commit() + .await + .unwrap(); + + let mut tuf = Database::::from_trusted_metadata(&metadata).unwrap(); + + //// build delegation B //// + + let delegations = Delegations::builder() + .key(delegation_b_key.public().clone()) + .role( + Delegation::builder(MetadataPath::new("delegation-b").unwrap()) + .key(delegation_b_key.public()) + .delegate_path(TargetPath::new("foo").unwrap()) + .build() + .unwrap(), + ) + .build() + .unwrap(); + + let delegation = TargetsMetadataBuilder::new() + .delegations(delegations) + .signed::(&delegation_a_key) + .unwrap(); + let raw_delegation = delegation.to_raw().unwrap(); + + tuf.update_delegated_targets( + &now, + &MetadataPath::targets(), + &MetadataPath::new("delegation-a").unwrap(), + &raw_delegation, + ) + .unwrap(); + + //// build delegation B //// + + let target_file: &[u8] = b"bar"; + + let delegation = TargetsMetadataBuilder::new() + .insert_target_from_slice( + TargetPath::new("foo").unwrap(), + target_file, + &[HashAlgorithm::Sha256], + ) + .unwrap() + .signed::(&delegation_b_key) + .unwrap(); + let raw_delegation = delegation.to_raw().unwrap(); + + tuf.update_delegated_targets( + &now, + &MetadataPath::new("delegation-a").unwrap(), + &MetadataPath::new("delegation-b").unwrap(), + &raw_delegation, + ) + .unwrap(); + + assert!(tuf + .target_description(&TargetPath::new("foo").unwrap()) + .is_ok()); + }) +} + +#[test] +fn rejects_bad_delegation_signatures() { + block_on(async { + let now = Utc::now(); + + let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let snapshot_key = Ed25519PrivateKey::from_pkcs8(ED25519_2_PK8).unwrap(); + let targets_key = Ed25519PrivateKey::from_pkcs8(ED25519_3_PK8).unwrap(); + let timestamp_key = Ed25519PrivateKey::from_pkcs8(ED25519_4_PK8).unwrap(); + let delegation_key = Ed25519PrivateKey::from_pkcs8(ED25519_5_PK8).unwrap(); + let bad_delegation_key = Ed25519PrivateKey::from_pkcs8(ED25519_6_PK8).unwrap(); + + let mut repo = EphemeralRepository::new(); + let metadata = RepoBuilder::create(&mut repo) + .trusted_root_keys(&[&root_key]) + .trusted_snapshot_keys(&[&snapshot_key]) + .trusted_targets_keys(&[&targets_key]) + .trusted_timestamp_keys(&[×tamp_key]) + .stage_root() + .unwrap() + .add_delegation_key(delegation_key.public().clone()) + .add_delegation_role( + Delegation::builder(MetadataPath::new("delegation").unwrap()) + .key(delegation_key.public()) + .delegate_path(TargetPath::new("foo").unwrap()) + .build() + .unwrap(), + ) + .stage_targets() + .unwrap() + .stage_snapshot_with_builder(|builder| { + builder.insert_metadata_description( + MetadataPath::new("delegation").unwrap(), + MetadataDescription::from_slice(&[0u8], 1, &[HashAlgorithm::Sha256]).unwrap(), + ) + }) + .unwrap() + .commit() + .await + .unwrap(); + + let mut tuf = Database::::from_trusted_metadata(&metadata).unwrap(); + + //// build the delegation //// + let target_file: &[u8] = b"bar"; + let delegation = TargetsMetadataBuilder::new() + .insert_target_from_slice( + TargetPath::new("foo").unwrap(), + target_file, + &[HashAlgorithm::Sha256], + ) + .unwrap() + .signed::(&bad_delegation_key) + .unwrap(); + let raw_delegation = delegation.to_raw().unwrap(); + + assert_matches!( + tuf.update_delegated_targets( + &now, + &MetadataPath::targets(), + &MetadataPath::new("delegation").unwrap(), + &raw_delegation + ), + Err(Error::MetadataMissingSignatures { + role, + number_of_valid_signatures: 0, + threshold: 1, + }) + if role == MetadataPath::new("delegation").unwrap() + ); + + let target_path = TargetPath::new("foo").unwrap(); + assert_matches!( + tuf.target_description(&target_path), + Err(Error::TargetNotFound(p)) if p == target_path + ); + }) +} + +#[test] +fn diamond_delegation() { + block_on(async { + let now = Utc::now(); + + let etc_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8).unwrap(); + let targets_key = Ed25519PrivateKey::from_pkcs8(ED25519_2_PK8).unwrap(); + let delegation_a_key = Ed25519PrivateKey::from_pkcs8(ED25519_3_PK8).unwrap(); + let delegation_b_key = Ed25519PrivateKey::from_pkcs8(ED25519_4_PK8).unwrap(); + let delegation_c_key = Ed25519PrivateKey::from_pkcs8(ED25519_5_PK8).unwrap(); + + // Given delegations a, b, and c, targets delegates "foo" to delegation-a and "bar" to + // delegation-b. + // + // targets + // / \ + // delegation-a delegation-b + // \ / + // delegation-c + // + // if delegation-a delegates "foo" to delegation-c, and + // delegation-b delegates "bar" to delegation-c, but + // delegation-b's signature is invalid, then delegation-c + // can contain target "bar" which is unaccessible and target "foo" which is. + // + // Verify tuf::Database handles this situation correctly. + + //// build delegation A //// + + let delegations_a = Delegations::builder() + .key(delegation_c_key.public().clone()) + .role( + Delegation::builder(MetadataPath::new("delegation-c").unwrap()) + .key(delegation_c_key.public()) + .delegate_path(TargetPath::new("foo").unwrap()) + .build() + .unwrap(), + ) + .build() + .unwrap(); + + let delegation_a = TargetsMetadataBuilder::new() + .delegations(delegations_a) + .signed::(&delegation_a_key) + .unwrap(); + let raw_delegation_a = delegation_a.to_raw().unwrap(); + + //// build delegation B //// + + let delegations_b = Delegations::builder() + .key(delegation_c_key.public().clone()) + .role( + Delegation::builder(MetadataPath::new("delegation-c").unwrap()) + // oops, wrong key. + .key(delegation_b_key.public()) + .delegate_path(TargetPath::new("foo").unwrap()) + .build() + .unwrap(), + ) + .build() + .unwrap(); + + let delegation_b = TargetsMetadataBuilder::new() + .delegations(delegations_b) + .signed::(&delegation_b_key) + .unwrap(); + let raw_delegation_b = delegation_b.to_raw().unwrap(); + + //// build delegation C //// + + let foo_target_file: &[u8] = b"foo contents"; + let bar_target_file: &[u8] = b"bar contents"; + + let delegation_c = TargetsMetadataBuilder::new() + .insert_target_from_slice( + TargetPath::new("foo").unwrap(), + foo_target_file, + &[HashAlgorithm::Sha256], + ) + .unwrap() + .insert_target_from_slice( + TargetPath::new("bar").unwrap(), + bar_target_file, + &[HashAlgorithm::Sha256], + ) + .unwrap() + .signed::(&delegation_c_key) + .unwrap(); + let raw_delegation_c = delegation_c.to_raw().unwrap(); + + //// construct the database //// + + let mut repo = EphemeralRepository::new(); + let metadata = RepoBuilder::create(&mut repo) + .trusted_root_keys(&[&etc_key]) + .trusted_snapshot_keys(&[&etc_key]) + .trusted_targets_keys(&[&targets_key]) + .trusted_timestamp_keys(&[&etc_key]) + .stage_root() + .unwrap() + .add_delegation_key(delegation_a_key.public().clone()) + .add_delegation_key(delegation_b_key.public().clone()) + .add_delegation_role( + Delegation::builder(MetadataPath::new("delegation-a").unwrap()) + .key(delegation_a_key.public()) + .delegate_path(TargetPath::new("foo").unwrap()) + .build() + .unwrap(), + ) + .add_delegation_role( + Delegation::builder(MetadataPath::new("delegation-b").unwrap()) + .key(delegation_b_key.public()) + .delegate_path(TargetPath::new("bar").unwrap()) + .build() + .unwrap(), + ) + .stage_targets() + .unwrap() + .stage_snapshot_with_builder(|builder| { + builder + .insert_metadata_description( + MetadataPath::new("delegation-a").unwrap(), + MetadataDescription::from_slice( + raw_delegation_a.as_bytes(), + 1, + &[HashAlgorithm::Sha256], + ) + .unwrap(), + ) + .insert_metadata_description( + MetadataPath::new("delegation-b").unwrap(), + MetadataDescription::from_slice( + raw_delegation_b.as_bytes(), + 1, + &[HashAlgorithm::Sha256], + ) + .unwrap(), + ) + .insert_metadata_description( + MetadataPath::new("delegation-c").unwrap(), + MetadataDescription::from_slice( + raw_delegation_c.as_bytes(), + 1, + &[HashAlgorithm::Sha256], + ) + .unwrap(), + ) + }) + .unwrap() + .commit() + .await + .unwrap(); + + let mut tuf = Database::::from_trusted_metadata(&metadata).unwrap(); + + //// Verify we can trust delegation-a and delegation-b.. + + tuf.update_delegated_targets( + &now, + &MetadataPath::targets(), + &MetadataPath::new("delegation-a").unwrap(), + &raw_delegation_a, + ) + .unwrap(); + + tuf.update_delegated_targets( + &now, + &MetadataPath::targets(), + &MetadataPath::new("delegation-b").unwrap(), + &raw_delegation_b, + ) + .unwrap(); + + //// Verify delegation-c is valid, but only when updated through delegation-a. + + assert_matches!( + tuf.update_delegated_targets( + &now, + &MetadataPath::new("delegation-b").unwrap(), + &MetadataPath::new("delegation-c").unwrap(), + &raw_delegation_c + ), + Err(Error::MetadataMissingSignatures { + role, + number_of_valid_signatures: 0, + threshold: 1, + }) + if role == MetadataPath::new("delegation-c").unwrap() + ); + + tuf.update_delegated_targets( + &now, + &MetadataPath::new("delegation-a").unwrap(), + &MetadataPath::new("delegation-c").unwrap(), + &raw_delegation_c, + ) + .unwrap(); + + assert!(tuf + .target_description(&TargetPath::new("foo").unwrap()) + .is_ok()); + + let target_path = TargetPath::new("bar").unwrap(); + assert_matches!( + tuf.target_description(&target_path), + Err(Error::TargetNotFound(p)) if p == target_path + ); + }) +} diff --git a/vendor/tuf/tests/rsa/gen.sh b/vendor/tuf/tests/rsa/gen.sh new file mode 100755 index 0000000000..b6534f183b --- /dev/null +++ b/vendor/tuf/tests/rsa/gen.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -eux + +cd "$(dirname "$0")" + +for key_size in 2048 4096; do + key="rsa-$key_size" + pk8="$key.pk8.der" + spki="$key.spki.der" + pkcs1="$key.pkcs1.der" + key="$key.der" + + if [ ! -f "$key" ]; then + openssl genpkey -algorithm RSA \ + -pkeyopt "rsa_keygen_bits:$key_size" \ + -pkeyopt rsa_keygen_pubexp:65537 \ + -outform der \ + -out "$key" + fi + + openssl rsa -in "$key" \ + -inform der \ + -RSAPublicKey_out \ + -outform der \ + -out "$pkcs1" + + openssl rsa -in "$key" \ + -inform der \ + -pubout \ + -outform der \ + -out "$spki" + + openssl pkcs8 -topk8 \ + -inform der \ + -in "$key" \ + -outform der \ + -out "$pk8" \ + -nocrypt +done diff --git a/vendor/tuf/tests/rsa/rsa-2048 b/vendor/tuf/tests/rsa/rsa-2048 new file mode 100644 index 0000000000000000000000000000000000000000..a2eec5b5f4530bfb8540f689bde24a3731a844a6 GIT binary patch literal 1192 zcmV;Z1Xueof&`=j0RRGm0RaHmMFAcz?6J!S;YYtulJH|=$2p;zpg;G=Ks6#7{*XQ! zPo&$EIUxvjqnwyJT-*kXFnpgZQ$VMFujcoEGpjg*9Aqci8=y(Oh4XZaDSPNL6)Pw~ z)`1QGv7cCi;)o57vn`-S`HGGX|B!0J)T%Qu8WYoEBb!u2OZ z9bJ_N@W)G61VQOWdqd)csdtCbs7yhO&EbaCvh{iZZ8(!&Ke$3m+9CT>%JC+0++H`5 z5N+OZ*n$@yP`t}u?1D~Bj_q$=XpTyaETYi*>AJkSkKqUhy#*V}cl~H~sYAZ=L2iG+ zsg#B+P|hqPtyh zcZ#T7!bxMedME6>`-OK0y{&gS0)c@5>yAp{%{zVS1Ceo&*1ey`U2}VS`K}pm7hmtw z_$1GQ&QeXCBUu7;ixhP=#ydFIb5Sqs{+3OJ^p2+66CSEmC@qWak6y9M>qPX0B1~}i z?C-mU)#>R^Fqg$paY)UtatnDh12oZ0IWc@SRg!~~D|2@tFT`1LiEg$bMF05(0)c@5 z>k2aBLAFUB<#RR2pz|F))QucUA;~|TE+#ih3xd>1AGH|WF>Wc1dlI^=9XNQXdn_(4 zEd25Pc7%MTuLl#IFUrnc@QyyXjCpd7MVIJq`qx-pIvD8=LW%2gXXLt2Lng-}$+1Yj zC;5Bi$on*-l&<^BW4D>MQWY}S`Vq;S0)c@5wS+SS38>tFraM3H2r-+Rx{I+5w6%(N zA)pp1o4}F()QRh{IeG8EYb08x6PdGBqfuVN8}us89R-MDtve<M7ry9mSW2rMf0G66-0a&hCO-cWJDh-`oHC4448Xapt%WNY z0)c>KvG4jld}V)2gW=79_Ak7|?wLaB18ru|#PB~gW6}O0o%OgL-w#uZ;JU0OsC{fm ze*r8pg76_e+H52<>eubI^aTD`O7oCj7hWBBw-t7eN(GIu$$o7S6(g=ljf$U_u6vU0 zxvN9FfD*qJ#=0xq^(i$2Tsc9_nkIRaRfXvSfq?+HHP1|q?$SCysN@8pXyyWLS^^T` zcymS#-}M-t4;d)X&7XM)IulsnR<0tpXI)^4J5%tIT=3v- z+eRSLaZf%AY38^}9l$#1N2nd#(}*?e)3BPgB}*|^O~ra6w@yi_PAD{{4YE1WkzZ0a GO{sXuHAe~n literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/rsa/rsa-2048.der b/vendor/tuf/tests/rsa/rsa-2048.der new file mode 100644 index 0000000000000000000000000000000000000000..a932df046fe138eb4efda1905878876d9c635772 GIT binary patch literal 1194 zcmV;b1XcSmf&``l0RRGm0RaHdjjuT&HCVhw-u|{Jxt<9)TTOSd&!_(~fyW-y+IJ}N zKtl_p8rmbkbv6rJn7ONy#dt*CV@WbPWEj_!&Myr%zi%u|W5#$qn;~6aVO&s^!#I0# zjt8G;N#sU}k|)s?MlG!Bw$I~;R0Vw0R z4hAzDuQk_~6|ZAs9f<1e9wRFR9szKlnkbrwIAni%1*e_nDefwyLSEg@mfi&A!DFbv z2fVi#4`!QC=XmxLHgOb}@~A0O_Q1HtRiKZJXhRI$1_+j{J(X~}x+T(3Z8}K=tZM$w z_JQ6wKU#};>Wl4)-I-4nP3L4>;7=mk_Y$yS#FUI;6NR7SKSp7EdTqysk9O-^)FrD| zpFG%Wg>FoB=rGF6ygC0P{eV$7OJ-3rC3iBg(oqOyuF1KDpCRsghSf z{NYqZ$5Jtres{=MsTU6-F=Q7p(!Z6T1F0}?RWP=*fb&}I!H8T6_yS3=>Cdw6HeSQe zp&g$D%;wrnMVwTpXnvwT#WjrWo#5MBn)foY&kpG`ySJG)n^p%Aww~)@KJU7-hXR3t z0NQRWA8;#BY%%$6rt;|=+f4^hlm(t1($0xsoyDz#?E0b!vJ0_t5rqf+0l!z0T~&Qk zfE$P*9gGWIVAT2>H_%;>!aub30CLg}QX^0c0PV~VInZ`+b?BN7Yw1GGkYCy$NJjG3XrdYpz<8cx;;2}uG%xaCi{#yd_EE@Hw4xP1zp&{sxG=@8{q zQv!j30L%#y$|M^xx_?XpB8<^k6*=_PJ`br*9hkHhz6rxMa*hN7j(`ET4a#^`%jObA zG)@5xn4=#X-`~qs>&P`v8?Z2vh;c|pq+)gUQy^A8 z$6rGF__}w8LQ8ML-<ygC0G8pBCb#j?He7;JnAI!$iMGe( z%WSp!P)E=ss`{bQLv|VVn&LNQUrsxX2CrUC%~0)hbn0MCuDIUzMz zyhYyrwkf%u2{>C#cd^f>|1yEc9@N@*DDgl;3#1y_Bfxbw3tX7FtCPieMBZabGCE`! z*ObmL4K}}TEKFm@cs!dSU0-2bP?f_tdvcBkpJ++sMv0Oq(H2H6tm?MUA zA~o6tlw5i$E#;W-L7H-_g1t}STG8)Dd=`&f=D?bY$qhmtQl?+%h@iDp95k0=>`j3l zOblx>l#!67jNs0Sj-rFU#;j?Tk*s6sMH}Y!O>c8Wlo?l4t7}^JQhP&)Pj^<|K0(Xi ze-wH!k;+vgz!6+?%Q?xKNk#}|29N=7Y&A~pR&~foK0!Nti}mpYZvq1W009Dm0RaH0 z(n~Xe@_ljcw_2fFK@#oO{8azO4(&`Mjq)S@=#WAxF{T$n?O=cM9Hh_-3S$G$m$0Jy z?C@>9mkX)Z?E2D~w2rg*mPD+C?^aG#ne znuj=Ke|iO{o#rX-Dx^YQ-OiTY1m(eFsK5ujw;2y+n^5O?_7gU76qoX-DO2{qxW-kW zkBw+U4BZ9@maILMaJsrB(ok(WNd&BF{?7J+-Z(#6i+Jjb?TX!*PZdq)WL)4+BHQ;8 zuwlfMjA9dopW{D9VSIXR$A*t~>s-_&t5=^q*lUGuOm*lm%FN_ZbmV-Gn<%jYfq?+? zK$XX3aH$JNi2EbTx;9MY;g~+T(@&|AS3vyXR7J;9F_eCH$XBTs4tR0cy0eD@fq?+pZY&>gD^P4P`EI82=^Wcl2T+s+o*vT9 ziC~?@t%L0Pq6xAKv2qcG2mJxRSCU;-eN%uNh#?(}3teE;`WrXUU68^*wDtgU(hgE1 zPzwO<%nv!xc5rp*nhtB}Ld}q0+8|0onYQ=dM|*9zOTF8`ZT`MGQcc5L1q58to-x@1 zfq?+G{o4%IVz0_`QeHcSWbzxCXf^``1)P-gw*l4#4g58>s{h&K`f01-U^WTM-fMO+ zDVY3d<<#Iv7kQBh0e2$Oe_1OkqL0k;jxcvZ{h5=Asl0S%a=9~59iDXb(Q@IW zU$*4l`-|(yHBTF`Fp`LINJgY$b@o#rRzAmHLi+f+cZWhtZ^GZ4_M&*pJ<3V!kozn^ z+D5Sgfq?*);gTk|@zFM1f>W5)EBlGI$K}gxwfay;&?Bn)q0&Qk8TOkcm|kt!(iPX8 zLYknzE#gu2!UCwJMH75fsIBAO=GEb@EO<5w&+;Ey8IL=aisiq-&aRqm0)vSEm~+EC iJ7%Z}UP>X`mBERQi>6+Oes~Q!)(>`R+>x!k?!}Av@k>(x literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/rsa/rsa-2048.pkcs1.der b/vendor/tuf/tests/rsa/rsa-2048.pkcs1.der new file mode 100644 index 0000000000000000000000000000000000000000..9793d7e1e4e3921bc2a3605f1bbe7663f6598b26 GIT binary patch literal 270 zcmV+p0rCDYf&mHwf&l>l&yBA+AvIXMMc)3lDY>2rI9pA3vCpUfGJ(e))Y^9_@jych zq#D{Iz;!kYT$s75lf`&M-eXBJI%F8vl+G^=HotEyOk>7)Jewh1UtwHOmBToDa*hX| zXi4NoiIOML7Dg?s>bB40h*SlA9c2n4HQEJ~TzV=k<(Tk6nsTdxy-(p<(eFll7LQ!! zz?zE54MHALreElYptV#SG?!xRO@SUv3~Mr!k&vZ~;LeJUqJzH1tZ9{ztYhg#8|L;+ zZ*xVI8CO)RYg+bFdqap%cUIp%LCfHO6nZd`%2gx45nOZ2ImwzyMhImFkO6OOHBRkT Ub;w9QK|6el_3;F60s{d60Tz#V*8l(j literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/rsa/rsa-2048.spki.der b/vendor/tuf/tests/rsa/rsa-2048.spki.der new file mode 100644 index 0000000000000000000000000000000000000000..f57e69dadedd6ab9b24e208d0b41ee2faeb7a4b3 GIT binary patch literal 294 zcmV+>0ondAf&n5h4F(A+hDe6@4FLfG1potr0S^E$f&mHwf&l>l&yBA+AvIXMMc)3l zDY>2rI9pA3vCpUfGJ(e))Y^9_@jychq#D{Iz;!kYT$s75lf`&M-eXBJI%F8vl+G^= zHotEyOk>7)Jewh1UtwHOmBToDa*hX|Xi4NoiIOML7Dg?s>bB40h*SlA9c2n4HQEJ~ zTzV=k<(Tk6nsTdxy-(p<(eFll7LQ!!z?zE54MHALreElYptV#SG?!xRO@SUv3~Mr! zk&vZ~;LeJUqJzH1tZ9{ztYhg#8|L;+Z*xVI8CO)RYg+bFdqap%cUIp%LCfHO6nZd` s%2gx45nOZ2ImwzyMhImFkO6OOHBRkTb;w9QK|6el_3;F60s{d60nYw?7XSbN literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/rsa/rsa-4096.der b/vendor/tuf/tests/rsa/rsa-4096.der new file mode 100644 index 0000000000000000000000000000000000000000..44cbedb892e07f9ce56a095efca36961da4f1a89 GIT binary patch literal 2348 zcmV+{3Dfp4f(a-B0RRGm0s#Q`U%CjHfzG!2I5d|&^-71*6}&kE>vwto0ouYnSqE*H z(H_9Uq$f%NcO~y{~ZL<~kk+|_?f)p(H?%@Na;zqtEls#Q6i)8wrcN0tCWADk2 zf;9BAn}7|caBM$qNm#dc!RFTHo`}%O8qmQ+F9#E@{ zCh7}XibMlvH1&Ys6=B3ZGAJ0;f!bf{$$rO9O>lqZiv4Nl-gm3b(II2kNBK=IAG=Nj z6+*~Du~MbO8l3JeEKV32NnNeePNK&pVh+v{Bd3ygtzUO(!Y0`Q4!iwz5GBN&G1Cu{(-!NkI2F(W*(NfI4}z><6RJP&rN|jA@XbM@ zvkUEthS^AiX|*LJJu7)%U+-2Vu6+VMe(KtD)E-rFZn9=PF``$fryXV1Fa60{K4Uy^ zHJGRVj5ua$1^kGKPmtv@+Il?scbv%o!riqvQmYUr;KX?$@VD_@*HQr;Gm0QhQ+6aa z17(>w1zs1I7;$aI8<-RG2rXB>bq)}vv3^SMUdJx{%b4c;2+lI?bsd^V>(%Y<*@9ZCFW2p zeJc0}#-0S5va?0!F{7!_f~NPy%|ytX{>~do@pmn`!4vUewKem_fa6U!w6Q zt3h6!K-N|hETFk5)hMVGL^~eB09(Kv(?vp5nuiS;#+up7q)$`|uR`SiW5+>QCYvQ~ z@IpjtNEem5qya_0*@I^!u3~E*L!cX+J;n@Ps>N>Or>5N5R-~Z03>=`xbCBvE5??iC zu@OAC<`64{aw*cm0)hbn0Q?jm!{r>+*7t%wM&7Wg?(Tq$%}VG--Tv%*vr+Ury=@)` zWa{=27tGDfMhU3<2kTNZ7qIL_BpFlmwUgrD>Kc!2#o$%w1Z_d4G;3Y^g0siYz4Qp{v zQ+ytsM(SP3HDsXU_FH@bW{4n8y?|SC?F1sD3XxdMU#0Ra0r-Quo*jfg*!VzxC2tgxh3Cj2z}Y=j<#N=c}F1GLY7 zgYLJfa;nGUc2pwUZ_G+c_-e9@g{Dd@dcw~iBmX5J8N%*8%0Dueso(FygV z@aaHH?EeZERKYdTPM4&S z!8pC=rF}tq0`nd3kjt3k2ace0)3d(AvTtPUOO#P@eVn6y1AF7$!7|=pcpHt-|WiZ~#@|*wTrVD74 z-MBVaN7XS5ERa)r6-feu0RUqUPhR~LFS^s_*ZZXUMW6YN2Od}b^b$P2u0~NS%5+zS zo{m`Ab|0l?DcFxr8_i%=Ba&YUP67TYf7Yc$?J0U4Ti4;;(rTPhqVwt$DfoMw)C?Ue z+VeQ47~Hn7S8>1oc{4d|UfdAMb~TDbv8jn*lAczc)}k7>U*i1rfND$prlD)`H)JMO zh0^?Ay->-x-`zdyW1{Eh$5Gi&n^oPAt__XMTXxAe&LClkc!z_|g9sb98G5Cx7prHo z*J1)XZXY`7Z36+*Wg)!#yyl~^dYOhz*&q%{y3+uNDmF8DTn&XjFG2}~cdnoO*W=Aw z$?ojtE;@4j{S`F2azj)U{>=hJipfE#L9O1SJ(5D7lIX=y>mZBwyIQC58F zbjMcYvcRAEPMCs0HI}nHuO3nX8~4V{Gkuh^=3`^Y&9M$L$7KN> ze)rCXZq}tmPgQrcuj85}E6s SG4&&mQ$6fN7V^^&S6JmuqJJp> literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/rsa/rsa-4096.pk8.der b/vendor/tuf/tests/rsa/rsa-4096.pk8.der new file mode 100644 index 0000000000000000000000000000000000000000..35a8247dca11c2b23d6a11b3819f969825abf106 GIT binary patch literal 2374 zcmV-M3Ay$#f(b$b0RS)!1_>&LNQUwEHHuzC;|Zh0)heo0QX!0o>!MqbFMl6OtUPVsHC759<2@nwP( zEcou>1Eu0dz9y7CT`Y@a`k!|bOWu;O#bimUNVC3kA5xo16`uCunne zi@RiNG6|jSfHoq=xyfQ<@hj}jV@LEVuvHby4L!;14W|`)7H#sjDwdeHimTNE|h$9YZ zh^pw+t0B$XyStaB*(O&f@>CsQ$+&o$_M_r$c3Km{Zo2`x`4M;8SEI5ds4N009Dm0sw1Jl=Tc7 zJ0Um~&;r>eGKvp^tY8zWKkuc;87=V5L87w@?TUukNP}s$B_us7d0$`eRwS-{0zH1} z+H=$%RdH^zW;`*XSE#2QW!5kK$yz>RJa9Fbr~ZsMW@-ieh>1^-CMwHpD+J!(M<_=1l6lYL~Nc9NR^Z z+BN=bE9KxH*5&Qk9u#9bS%BrR?4Kp(P%M2a_z1?H1e>z6MdvZ2snCL^_r=Xb$eaGo z8%gp+pa5@l4UlRUw;M`t)~zx=tbl7Kc=*@1MwXS8yAZQc6_2r+E^`*ar49Sox0f(2 z%`2W*h)6XuQuA74qr`{c$ zMvg5Xzv07eucVu4=S*JI&3u?ay>4Hk@g}Q5UY$VJRue3sxhd5ss1!sy9>M@yz#Y>? zLR6ZE4H?Fo*~_F)R0^*`MZejDXC$s-YaT+}T#7pt%ejpvQBN>K_tcHD$38Jh$c$D}-_>(!m0P0RaH~6d%Lo9M#tM zf<8vxu&M6ufQ-#b=tkZC?0d6O^gF$69tULV_7NA;9P@TpsE;db?{&gocHlQZXEoXQ zx1}fUzgNPvM>KIo3aX&;s!XB9B<$wX6(miqD-oZ0kR~IM&bDZ-3NX4o{!5j%_fml* zN4b-TY}rIpctpv?{j}Q<+1~tYiAKl*a}bB_(9!AgOGAd^)@zV5Eo`E83z45dJ;z?= z{0Tp$OSzr!roABaZgix9-T(&UCA|MpyT#id;w;NAWpr2Td!so zmzdP^C2YPK4QRfm;i{K$4b28yHkt&Jrld4zaz;0$3nv8^`n0(Mf&l>l`#9a=u7Hh* zKa*m%H3_V+q*f;UH2Z9X9)(ItsC@&p&wqpNx2bZf$K!TXBHM4wN=o=@vW$hMN-TQ9 z&pelIx!(;Dp=2@_KVl#VUj@V$z*WBc=_+Asdf~2zJhEU2d1@QqoW>zQ0_yGG6Lndb zXx^u}J;aj*2Bzb01l#2y$ll=>$LgY@;HkHsixQN~btW<1r!STA3>?Cz`gepwuxj!7 zlt%?{q*^Xg?!ac=5NyoFJ})YR``ytA^`r3VKuhfZ3Kv$dRJk6XGCgJg;}aS)%%i2Y zXy^KCiFn;plbK%e#mR0N0)hbmcI68Kdy4rr zRx)8LP8X+NA+ZuTHV=+;fmz2~KUL3#r79q*9KnBxbIpmfu-zQeL_ zWb8|nQE`2oqkaQ>J%yXdz{n^9V*)MIHwrgwy;-mzy5hMIc#3s5XyEnibS!g ziC~hRR-V?P8n<8K{PlopOZ=vxYw!*-x8Q-H@&g zjm%qi$v4g*VTgE#gU*8p8@CyHrK}gLXR_B~0y=IVI_Yf#0n=q6y!*W7qp^CKhE3TZ z4oSMx0Ej9!GkIJMg+4Dr350j9pZwS3&0ERt?B*^ya{T=jG`e!J0)hbn0NhL>qnj%~ ziqItvLs?`tJ|;f+kX9@WDvhzU*E?m^J^^c@KS_GNU=ZRscy2_Du{Ad4KDudXLB?%Uu}V=^eCl=2uZyLxCnskmRR!w<2`aL{pZZRif_K)l(XhzW69024l>7O0Udt#&aP^^{il0^tsGmNN}*9>Vk>!g{^E~8 zDzy(~lQ%NlKj;;ld%K7{^`@HJWWhAVyc@yD@3|9q62N si`{$5?GUe-C7|k*3CgVH9u%fg%R~~H>z*<7Bal-)>_isw(-2o!<#7swx&QzG literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/rsa/rsa-4096.pkcs1.der b/vendor/tuf/tests/rsa/rsa-4096.pkcs1.der new file mode 100644 index 0000000000000000000000000000000000000000..92956140432861dd84867a4bf89c13256d80a1e6 GIT binary patch literal 526 zcmV+p0`dJYf&vNxf&u{m_g}gQnSsu>`ZzR~KJ`k6(iOZp1M7Er{{h;<=#V4Xn~sXsTX8xk!zgd~|w zld`1X?KXUtbd+oh1-QqXoDH2PXmfgtyJTxJ2$=uq?G8mzckXi8h~Y)>OC%{;R3_>R zT8cyiXEgPI;1yxSJu)a5)q&by>dAh`PEBxs<%<1j=iYa#&Cwxa*GKtHE+4y21QkNa zLa|b%!y26KEi6tL8cAKP(@vttC1MWF5+kRQcdcJ{Y2>PUWjFAs;JKXLz~c_)RBRQ> zIf0>4^`lEIdrNnQU|p<1-u5aKIB#Sucp$3mU`IbTzb)BMxbZs_4|KA~;oTi2s!)Vp_gdQs9=4Wrhncxc-MKO~beq{iEGu~AB z7Zc*q(pw7}b0u7%xxArVLXekR*sv#xS(mu=+XC>3Y>N#GY$F(k=_hOev*cO*CY|tF QRzJ|-Q)DC&0s{d60k66K5dZ)H literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/rsa/rsa-4096.spki.der b/vendor/tuf/tests/rsa/rsa-4096.spki.der new file mode 100644 index 0000000000000000000000000000000000000000..2503d55289321f1793f0ac7d48125f339293543a GIT binary patch literal 550 zcmV+>0@?jAf&wBi4F(A+hDe6@4FLfG1potr0uKN%f&vNxf&u{m_g}gQnSsu>`ZzR~ zKJ`k6(iOZp1M7Er{{h;<=#V4Xn~sXsTX8xk!zgd~|wld`1X?KXUtbd+oh1-QqXoDH2PXmfgtyJTxJ z2$=uq?G8mzckXi8h~Y)>OC%{;R3_>RT8cyiXEgPI;1yxSJu)a5)q&by>dAh`PEBxs z<%<1j=iYa#&Cwxa*GKtHE+4y21QkNaLa|b%!y26KEi6tL8cAKP(@vttC1MWF5+kRQ zcdcJ{Y2>PUWjFAs;JKXLz~c_)RBRQ>If0>4^`lEIdrNnQU|p<1-u5aKIB#Sucp$3m zU`IbTzb)BMxbZs_4|K zA~;oTi2s!)Vp_ zgdQs9=4Wrhncxc-MKO~beq{iEGu~AB7Zc*q(pw7}b0u7%xxArVLXekR*sv#xS(mu= o+XC>3Y>N#GY$F(k=_hOev*cO*CY|tFRzJ|-Q)DC&0s{d60j#J3VgLXD literal 0 HcmV?d00001 diff --git a/vendor/tuf/tests/simple_example.rs b/vendor/tuf/tests/simple_example.rs new file mode 100644 index 0000000000..40cdcd6010 --- /dev/null +++ b/vendor/tuf/tests/simple_example.rs @@ -0,0 +1,95 @@ +use futures_executor::block_on; +use futures_util::io::Cursor; +use tuf::client::{Client, Config}; +use tuf::crypto::{Ed25519PrivateKey, PrivateKey, PublicKey}; +use tuf::interchange::Json; +use tuf::metadata::{MetadataVersion, TargetPath}; +use tuf::repo_builder::RepoBuilder; +use tuf::repository::EphemeralRepository; +use tuf::Result; + +// Ironically, this is far from simple, but it's as simple as it can be made. + +const ED25519_1_PK8: &[u8] = include_bytes!("./ed25519/ed25519-1.pk8.der"); +const ED25519_2_PK8: &[u8] = include_bytes!("./ed25519/ed25519-2.pk8.der"); +const ED25519_3_PK8: &[u8] = include_bytes!("./ed25519/ed25519-3.pk8.der"); +const ED25519_4_PK8: &[u8] = include_bytes!("./ed25519/ed25519-4.pk8.der"); + +#[test] +fn consistent_snapshot_false() { + block_on(async { + let config = Config::default(); + + run_tests(config, false).await + }) +} + +#[test] +fn consistent_snapshot_true() { + block_on(async { + let config = Config::default(); + + run_tests(config, true).await + }) +} + +async fn run_tests(config: Config, consistent_snapshots: bool) { + let mut remote = EphemeralRepository::new(); + let root_public_keys = init_server(&mut remote, consistent_snapshots) + .await + .unwrap(); + + init_client(&root_public_keys, remote, config) + .await + .unwrap(); +} + +async fn init_client( + root_public_keys: &[PublicKey], + remote: EphemeralRepository, + config: Config, +) -> Result<()> { + let local = EphemeralRepository::new(); + let mut client = Client::with_trusted_root_keys( + config, + MetadataVersion::Number(1), + 1, + root_public_keys, + local, + remote, + ) + .await?; + let _ = client.update().await?; + let target_path = TargetPath::new("foo-bar")?; + client.fetch_target_to_local(&target_path).await +} + +async fn init_server( + remote: &mut EphemeralRepository, + consistent_snapshot: bool, +) -> Result> { + // in real life, you wouldn't want these keys on the same machine ever + let root_key = Ed25519PrivateKey::from_pkcs8(ED25519_1_PK8)?; + let snapshot_key = Ed25519PrivateKey::from_pkcs8(ED25519_2_PK8)?; + let targets_key = Ed25519PrivateKey::from_pkcs8(ED25519_3_PK8)?; + let timestamp_key = Ed25519PrivateKey::from_pkcs8(ED25519_4_PK8)?; + + let target_path = TargetPath::new("foo-bar")?; + let target_file: &[u8] = b"things fade, alternatives exclude"; + + let _metadata = RepoBuilder::create(&mut *remote) + .trusted_root_keys(&[&root_key]) + .trusted_snapshot_keys(&[&snapshot_key]) + .trusted_targets_keys(&[&targets_key]) + .trusted_timestamp_keys(&[×tamp_key]) + .stage_root_with_builder(|builder| builder.consistent_snapshot(consistent_snapshot)) + .unwrap() + .add_target(target_path.clone(), Cursor::new(target_file)) + .await + .unwrap() + .commit() + .await + .unwrap(); + + Ok(vec![root_key.public().clone()]) +} From cda641922e644c8d849e1f86ddbe6a7b6ec1cbbb Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 14:40:43 +0200 Subject: [PATCH 02/10] fix: address clippy lints in libdd-tuf-rust MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace deprecated chrono APIs throughout (in both production code and tests): - `Utc.ymd(y, m, d).and_hms(h, min, s)` → `Utc.with_ymd_and_hms(y, m, d, h, min, s).unwrap()` - `DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc)` → `DateTime::from_timestamp(0, 0).unwrap()` - Remove now-unused `use chrono::NaiveDateTime` import. - crypto: elide needless explicit lifetime `'a` on `retain_supported_hashes` - crypto: fix non-canonical `PartialOrd` impls on `PublicKey` and `Signature` to delegate to `Ord::cmp` (clippy::non_canonical_partial_ord_impl) - crypto: replace manual `BuildHasher`/`Hasher` pair in test with `BuildHasher::hash_one` (clippy::manual_hash_one) - metadata: replace `.into_iter().map(|(_k, v)| v)` on a `HashMap` with `.into_values()` (clippy::iter_kv_map) - metadata: fix doc-comment list-continuation indentation (4 items, clippy::doc_lazy_continuation) - lib: fix doc-comment list-continuation indentation (4 items) - repo_builder: remove redundant `.into_iter()` calls on `Vec` passed to `.extend()` (clippy::useless_conversion) - repository: replace `::std::usize::MAX` with `usize::MAX` (clippy::legacy_numeric_constants) - repository/ephemeral: remove redundant `.into_iter()` (clippy::useless_conversion) - repository/file_system: add explicit `'_` lifetime to `FileSystemBatchUpdate` return type (mismatched_lifetime_syntaxes); remove needless `&` borrows on `Path` args to `File::open` (clippy::needless_borrows_for_generic_args) - repository/http: elide needless `'a` lifetime on `get`; replace `io::Error::new(io::ErrorKind::Other, e)` with `io::Error::other(e)` (clippy::io_other_error) --- vendor/tuf/src/client.rs | 14 ++++----- vendor/tuf/src/crypto.rs | 18 ++++-------- vendor/tuf/src/lib.rs | 8 +++--- vendor/tuf/src/metadata.rs | 36 +++++++++++------------- vendor/tuf/src/repo_builder.rs | 27 ++++++++---------- vendor/tuf/src/repository.rs | 2 +- vendor/tuf/src/repository/ephemeral.rs | 4 +-- vendor/tuf/src/repository/file_system.rs | 6 ++-- vendor/tuf/src/repository/http.rs | 6 ++-- 9 files changed, 53 insertions(+), 68 deletions(-) diff --git a/vendor/tuf/src/client.rs b/vendor/tuf/src/client.rs index ae528e5d70..c1f79d704f 100644 --- a/vendor/tuf/src/client.rs +++ b/vendor/tuf/src/client.rs @@ -116,7 +116,7 @@ where /// let root_version = 1; /// let root = RootMetadataBuilder::new() /// .version(root_version) - /// .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + /// .expires(Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap()) /// .root_key(public_key.clone()) /// .snapshot_key(public_key.clone()) /// .targets_key(public_key.clone()) @@ -185,7 +185,7 @@ where /// let root_threshold = 1; /// let raw_root = RootMetadataBuilder::new() /// .version(root_version) - /// .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + /// .expires(Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap()) /// .root_key(public_key.clone()) /// .root_threshold(root_threshold) /// .snapshot_key(public_key.clone()) @@ -248,7 +248,7 @@ where /// let root_threshold = 1; /// let root = RootMetadataBuilder::new() /// .version(root_version) - /// .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + /// .expires(Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap()) /// .root_key(public_key.clone()) /// .root_threshold(root_threshold) /// .snapshot_key(public_key.clone()) @@ -1454,7 +1454,7 @@ mod test { .trusted_timestamp_keys(&[&KEYS[0]]) .stage_root_with_builder(|bld| { bld.consistent_snapshot(true) - .expires(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()) }) .unwrap() .commit_skip_validation() @@ -1730,7 +1730,7 @@ mod test { .stage_root_with_builder(|bld| { bld.version(1) .consistent_snapshot(true) - .expires(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()) }) .unwrap() .commit_skip_validation() @@ -1745,7 +1745,7 @@ mod test { .stage_root_with_builder(|bld| { bld.version(2) .consistent_snapshot(true) - .expires(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap()) }) .unwrap() .stage_targets_with_builder(|bld| bld.version(2)) @@ -1796,7 +1796,7 @@ mod test { .stage_root_with_builder(|bld| { bld.version(3) .consistent_snapshot(true) - .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap()) }) .unwrap() .stage_targets_with_builder(|bld| bld.version(2)) diff --git a/vendor/tuf/src/crypto.rs b/vendor/tuf/src/crypto.rs index ebf773b221..cd777d9e92 100644 --- a/vendor/tuf/src/crypto.rs +++ b/vendor/tuf/src/crypto.rs @@ -110,8 +110,8 @@ fn python_tuf_compatibility_keyid_hash_algorithms() -> Option> { /// ], /// ); /// ``` -pub fn retain_supported_hashes<'a>( - hashes: &'a HashMap, +pub fn retain_supported_hashes( + hashes: &HashMap, ) -> Vec<(&'static HashAlgorithm, HashValue)> { let mut data = vec![]; for alg in HASH_ALG_PREFS { @@ -943,7 +943,7 @@ impl Ord for PublicKey { impl PartialOrd for PublicKey { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.key_id.cmp(&other.key_id)) + Some(self.cmp(other)) } } @@ -1096,7 +1096,7 @@ impl Signature { impl PartialOrd for Signature { fn partial_cmp(&self, other: &Self) -> Option { - (&self.key_id, &self.value).partial_cmp(&(&other.key_id, &other.value)) + Some(self.cmp(other)) } } @@ -1773,16 +1773,10 @@ mod test { } fn check_public_key_hash(key1: &PublicKey, key2: &PublicKey) { - use std::hash::{BuildHasher, Hash, Hasher}; + use std::hash::BuildHasher; let state = std::collections::hash_map::RandomState::new(); - let mut hasher1 = state.build_hasher(); - key1.hash(&mut hasher1); - - let mut hasher2 = state.build_hasher(); - key2.hash(&mut hasher2); - - assert_ne!(hasher1.finish(), hasher2.finish()); + assert_ne!(state.hash_one(key1), state.hash_one(key2)); } #[cfg(feature = "unstable_rsa")] diff --git a/vendor/tuf/src/lib.rs b/vendor/tuf/src/lib.rs index 89624f151e..fd492370cc 100644 --- a/vendor/tuf/src/lib.rs +++ b/vendor/tuf/src/lib.rs @@ -11,9 +11,9 @@ //! actually implement TUF for a community repository. //! //! - [The Diplomat paper -//! (2016)](https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/kuppusamy) +//! (2016)](https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/kuppusamy) //! - [The Mercury paper -//! (2017)](https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy) +//! (2017)](https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy) //! //! Failure to read the spec and the above papers will likely lead to an implementation that does //! not take advantage of all the security guarantees that TUF offers. @@ -81,11 +81,11 @@ //! 2. `rarely-updated-projects` //! - Terminating //! - Signs all packages for all projects that have been "abandoned" or left unupdated for a long -//! time AND have not yet registered keys with TUF +//! time AND have not yet registered keys with TUF //! 3. `new-projects` //! - Non-terminating //! - Signs all packages for all new projects as well as projects that were relegated to -//! `rarely-updated-projects` +//! `rarely-updated-projects` //! //! The top-level `targets` role as well as `claimed-projects` and `rarely-updated-projects` //! **MUST** all use offline keys. diff --git a/vendor/tuf/src/metadata.rs b/vendor/tuf/src/metadata.rs index d62671e5c8..06a1472b5b 100644 --- a/vendor/tuf/src/metadata.rs +++ b/vendor/tuf/src/metadata.rs @@ -420,11 +420,7 @@ where /// Construct a new `SignedMetadata` using the included signatures, sorting the signatures by /// `KeyId`. pub fn build(self) -> SignedMetadata { - let mut signatures = self - .signatures - .into_iter() - .map(|(_k, v)| v) - .collect::>(); + let mut signatures = self.signatures.into_values().collect::>(); signatures.sort_unstable_by(|a, b| a.key_id().cmp(b.key_id())); SignedMetadata { @@ -487,10 +483,10 @@ where /// hash of the returned bytes will match a hash included in, for example, a snapshot metadata /// file, as: /// * Parsing metadata removes unknown fields, which would not be included in the returned - /// bytes, + /// bytes, /// * DataInterchange implementations only guarantee the bytes are canonical for the purpose of - /// a signature. Metadata obtained from a remote source may have included different whitespace - /// or ordered fields in a way that is not preserved when parsing that metadata. + /// a signature. Metadata obtained from a remote source may have included different whitespace + /// or ordered fields in a way that is not preserved when parsing that metadata. pub fn to_raw(&self) -> Result> { let bytes = D::canonicalize(&D::serialize(self)?)?; Ok(RawSignedMetadata::new(bytes)) @@ -2508,7 +2504,7 @@ mod test { let timestamp_key = Ed25519PrivateKey::from_pkcs8(ED25519_4_PK8).unwrap(); let root = RootMetadataBuilder::new() - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .root_key(root_key.public().clone()) .snapshot_key(snapshot_key.public().clone()) .targets_key(targets_key.public().clone()) @@ -2859,7 +2855,7 @@ mod test { .unwrap(); let timestamp = TimestampMetadataBuilder::from_metadata_description(description) - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .build() .unwrap(); @@ -2891,7 +2887,7 @@ mod test { let description = MetadataDescription::new(1, None, HashMap::new()).unwrap(); let timestamp = TimestampMetadataBuilder::from_metadata_description(description) - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .build() .unwrap(); @@ -2964,7 +2960,7 @@ mod test { #[test] fn serde_snapshot_metadata() { let snapshot = SnapshotMetadataBuilder::new() - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .insert_metadata_description( MetadataPath::new("targets").unwrap(), MetadataDescription::new( @@ -3003,7 +2999,7 @@ mod test { #[test] fn serde_snapshot_optional_length_and_hashes() { let snapshot = SnapshotMetadataBuilder::new() - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .insert_metadata_description( MetadataPath::new("targets").unwrap(), MetadataDescription::new(1, None, HashMap::new()).unwrap(), @@ -3033,7 +3029,7 @@ mod test { fn serde_targets_metadata() { block_on(async { let targets = TargetsMetadataBuilder::new() - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .insert_target_from_slice( TargetPath::new("insert-target-from-slice").unwrap(), &b"foo"[..], @@ -3137,7 +3133,7 @@ mod test { .unwrap(); let targets = TargetsMetadataBuilder::new() - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .delegations(delegations) .build() .unwrap(); @@ -3181,7 +3177,7 @@ mod test { #[test] fn serde_signed_metadata() { let snapshot = SnapshotMetadataBuilder::new() - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .insert_metadata_description( MetadataPath::new("targets").unwrap(), MetadataDescription::new( @@ -3261,7 +3257,7 @@ mod test { let timestamp_key = Ed25519PrivateKey::from_pkcs8(ED25519_4_PK8).unwrap(); let root = RootMetadataBuilder::new() - .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap()) .root_key(root_key.public().clone()) .snapshot_key(snapshot_key.public().clone()) .targets_key(targets_key.public().clone()) @@ -3274,7 +3270,7 @@ mod test { fn make_snapshot() -> serde_json::Value { let snapshot = SnapshotMetadataBuilder::new() - .expires(Utc.ymd(2038, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap()) .build() .unwrap(); @@ -3286,7 +3282,7 @@ mod test { MetadataDescription::from_slice(&[][..], 1, &[HashAlgorithm::Sha256]).unwrap(); let timestamp = TimestampMetadataBuilder::from_metadata_description(description) - .expires(Utc.ymd(2017, 1, 1).and_hms(0, 0, 0)) + .expires(Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap()) .build() .unwrap(); @@ -3296,7 +3292,7 @@ mod test { fn make_targets() -> serde_json::Value { let targets = TargetsMetadata::new( 1, - Utc.ymd(2038, 1, 1).and_hms(0, 0, 0), + Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap(), hashmap!(), Delegations::default(), Default::default(), diff --git a/vendor/tuf/src/repo_builder.rs b/vendor/tuf/src/repo_builder.rs index fcaa540434..77d50e2e2d 100644 --- a/vendor/tuf/src/repo_builder.rs +++ b/vendor/tuf/src/repo_builder.rs @@ -1473,8 +1473,6 @@ where #[cfg(test)] mod tests { - use chrono::NaiveDateTime; - use crate::repository::RepositoryProvider; use { @@ -1707,7 +1705,7 @@ mod tests { let mut remote = EphemeralRepository::::new(); // First, create the metadata. - let expires1 = Utc.ymd(2038, 1, 1).and_hms(0, 0, 0); + let expires1 = Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap(); let metadata1 = RepoBuilder::create(&mut remote) .trusted_root_keys(&[&KEYS[0], &KEYS[1], &KEYS[2]]) .trusted_targets_keys(&[&KEYS[1], &KEYS[2], &KEYS[3]]) @@ -1825,7 +1823,7 @@ mod tests { // Create a new metadata, derived from the tuf database we created // with the client. - let expires2 = Utc.ymd(2038, 1, 2).and_hms(0, 0, 0); + let expires2 = Utc.with_ymd_and_hms(2038, 1, 2, 0, 0, 0).unwrap(); 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]]) @@ -2325,7 +2323,7 @@ mod tests { let mut remote = EphemeralRepository::::new(); // First, write some metadata to the repo. - let expires1 = Utc.ymd(2038, 1, 1).and_hms(0, 0, 0); + let expires1 = Utc.with_ymd_and_hms(2038, 1, 1, 0, 0, 0).unwrap(); let metadata1 = RepoBuilder::create(&mut remote) .trusted_root_keys(&[&KEYS[0]]) .trusted_targets_keys(&[&KEYS[1]]) @@ -2389,7 +2387,7 @@ mod tests { let mut db = Database::from_trusted_metadata(&metadata1).unwrap(); // Next, write another batch, but only have the timestamp, snapshot, and targets keys. - let expires2 = Utc.ymd(2038, 1, 2).and_hms(0, 0, 0); + let expires2 = Utc.with_ymd_and_hms(2038, 1, 2, 0, 0, 0).unwrap(); let metadata2 = RepoBuilder::from_database(&mut remote, &db) .trusted_targets_keys(&[&KEYS[1]]) .trusted_snapshot_keys(&[&KEYS[2]]) @@ -2434,14 +2432,13 @@ mod tests { (MetadataPath::timestamp(), MetadataVersion::None), metadata2.timestamp().unwrap().as_bytes(), ), - ] - .into_iter(), + ], ); assert_repo(&remote, &expected_metadata); // Now, only have the timestamp and snapshot keys online. - let expires3 = Utc.ymd(2038, 1, 3).and_hms(0, 0, 0); + let expires3 = Utc.with_ymd_and_hms(2038, 1, 3, 0, 0, 0).unwrap(); let metadata3 = RepoBuilder::from_database(&mut remote, &db) .trusted_snapshot_keys(&[&KEYS[2]]) .trusted_timestamp_keys(&[&KEYS[3]]) @@ -2477,14 +2474,13 @@ mod tests { (MetadataPath::timestamp(), MetadataVersion::None), metadata3.timestamp().unwrap().as_bytes(), ), - ] - .into_iter(), + ], ); assert_repo(&remote, &expected_metadata); // Finally, only have the timestamp keys online. - let expires4 = Utc.ymd(2038, 1, 4).and_hms(0, 0, 0); + let expires4 = Utc.with_ymd_and_hms(2038, 1, 4, 0, 0, 0).unwrap(); let metadata4 = RepoBuilder::from_database(&mut remote, &db) .trusted_timestamp_keys(&[&KEYS[3]]) .skip_root() @@ -2508,8 +2504,7 @@ mod tests { vec![( (MetadataPath::timestamp(), MetadataVersion::None), metadata4.timestamp().unwrap().as_bytes(), - )] - .into_iter(), + )], ); assert_repo(&remote, &expected_metadata); @@ -2521,7 +2516,7 @@ mod tests { block_on(async move { let mut repo = EphemeralRepository::::new(); - let expires = Utc.ymd(2038, 1, 4).and_hms(0, 0, 0); + let expires = Utc.with_ymd_and_hms(2038, 1, 4, 0, 0, 0).unwrap(); let hash_algs = &[HashAlgorithm::Sha256, HashAlgorithm::Sha512]; let delegation_key = &KEYS[0]; let delegation_path = MetadataPath::new("delegations").unwrap(); @@ -2781,7 +2776,7 @@ mod tests { block_on(async move { let mut repo = EphemeralRepository::::new(); - let epoch = DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc); + let epoch = DateTime::from_timestamp(0, 0).unwrap(); let root_expires = epoch + Duration::seconds(40); let targets_expires = epoch + Duration::seconds(30); let snapshot_expires = epoch + Duration::seconds(20); diff --git a/vendor/tuf/src/repository.rs b/vendor/tuf/src/repository.rs index 3b314a3678..064d768c87 100644 --- a/vendor/tuf/src/repository.rs +++ b/vendor/tuf/src/repository.rs @@ -417,7 +417,7 @@ where .repository .fetch_metadata(meta_path, version) .await? - .check_length_and_hash(max_length.unwrap_or(::std::usize::MAX) as u64, hashes)?; + .check_length_and_hash(max_length.unwrap_or(usize::MAX) as u64, hashes)?; let mut buf = Vec::new(); reader.read_to_end(&mut buf).await?; diff --git a/vendor/tuf/src/repository/ephemeral.rs b/vendor/tuf/src/repository/ephemeral.rs index ec03103584..f4f4220ba3 100644 --- a/vendor/tuf/src/repository/ephemeral.rs +++ b/vendor/tuf/src/repository/ephemeral.rs @@ -165,11 +165,11 @@ where pub fn commit(self) { self.parent_repo .metadata - .extend(self.staging_repo.metadata.into_iter()); + .extend(self.staging_repo.metadata); self.parent_repo .targets - .extend(self.staging_repo.targets.into_iter()); + .extend(self.staging_repo.targets); } } diff --git a/vendor/tuf/src/repository/file_system.rs b/vendor/tuf/src/repository/file_system.rs index 6324200998..650735340e 100644 --- a/vendor/tuf/src/repository/file_system.rs +++ b/vendor/tuf/src/repository/file_system.rs @@ -112,7 +112,7 @@ where /// Returns a [FileSystemBatchUpdate] for manipulating this repository. This allows callers to /// stage a number of mutations, and optionally write them all at once. - pub fn batch_update(&mut self) -> FileSystemBatchUpdate { + pub fn batch_update(&mut self) -> FileSystemBatchUpdate<'_, D> { FileSystemBatchUpdate { repo: self, metadata: HashMap::new(), @@ -138,7 +138,7 @@ where version: MetadataVersion, path: &Path, ) -> BoxFuture<'_, Result>> { - let reader = File::open(&path).map_err(|err| { + let reader = File::open(path).map_err(|err| { if err.kind() == io::ErrorKind::NotFound { Error::MetadataNotFound { path: meta_path.clone(), @@ -165,7 +165,7 @@ where target_path: &TargetPath, path: &Path, ) -> BoxFuture<'_, Result>> { - let reader = File::open(&path).map_err(|err| { + let reader = File::open(path).map_err(|err| { if err.kind() == io::ErrorKind::NotFound { Error::TargetNotFound(target_path.clone()) } else { diff --git a/vendor/tuf/src/repository/http.rs b/vendor/tuf/src/repository/http.rs index d4a53ecdcc..6563ed846e 100644 --- a/vendor/tuf/src/repository/http.rs +++ b/vendor/tuf/src/repository/http.rs @@ -208,7 +208,7 @@ where C: Connect + Clone + Send + Sync + 'static, D: DataInterchange, { - async fn get<'a>(&'a self, uri: &Uri) -> Result> { + async fn get(&self, uri: &Uri) -> Result> { match Request::builder() .uri(uri) .header("User-Agent", &*self.user_agent) @@ -253,7 +253,7 @@ where if status == StatusCode::OK { let reader = resp .into_body() - .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + .map_err(io::Error::other) .into_async_read() .enforce_minimum_bitrate(self.min_bytes_per_second); @@ -292,7 +292,7 @@ where if status == StatusCode::OK { let reader = resp .into_body() - .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) + .map_err(io::Error::other) .into_async_read() .enforce_minimum_bitrate(self.min_bytes_per_second); From bb29349b084c7b685ea35e55c2881304cad33f15 Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 14:41:07 +0200 Subject: [PATCH 03/10] style: apply libdatadog rustfmt settings to libdd-tuf-rust Reformat all source files in vendor/tuf/ with the workspace rustfmt configuration (max_width=100, wrap_comments=true, format_code_in_doc_comments=true, etc.). This commit is specific to the libdatadog vendoring and is not intended for upstream application. --- vendor/tuf/src/client.rs | 63 ++++++++-------- vendor/tuf/src/crypto.rs | 9 +-- vendor/tuf/src/database.rs | 11 +-- vendor/tuf/src/error.rs | 4 +- vendor/tuf/src/lib.rs | 6 +- vendor/tuf/src/metadata.rs | 71 ++++++++++++------ vendor/tuf/src/repo_builder.rs | 100 ++++++++++++------------- vendor/tuf/src/repository/ephemeral.rs | 8 +- vendor/tuf/src/repository/http.rs | 5 +- 9 files changed, 145 insertions(+), 132 deletions(-) diff --git a/vendor/tuf/src/client.rs b/vendor/tuf/src/client.rs index c1f79d704f..fb4afd714f 100644 --- a/vendor/tuf/src/client.rs +++ b/vendor/tuf/src/client.rs @@ -26,7 +26,9 @@ //! let local = FileSystemRepository::::new(PathBuf::from("~/.rustup"))?; //! //! let remote = HttpRepositoryBuilder::new_with_uri( -//! "https://static.rust-lang.org/".parse::().unwrap(), +//! "https://static.rust-lang.org/" +//! .parse::() +//! .unwrap(), //! HttpClient::new(), //! ) //! .user_agent("rustup/1.4.0") @@ -39,7 +41,8 @@ //! &root_public_keys, //! local, //! remote, -//! ).await?; +//! ) +//! .await?; //! //! let _ = client.update().await?; //! # Ok(()) @@ -126,17 +129,15 @@ where /// let root_path = MetadataPath::root(); /// let root_version = MetadataVersion::Number(root_version); /// - /// local.store_metadata( - /// &root_path, - /// root_version, - /// &mut root.to_raw().unwrap().as_bytes() - /// ).await?; + /// local + /// .store_metadata( + /// &root_path, + /// root_version, + /// &mut root.to_raw().unwrap().as_bytes(), + /// ) + /// .await?; /// - /// let client = Client::with_trusted_local( - /// Config::default(), - /// local, - /// remote, - /// ).await?; + /// let client = Client::with_trusted_local(Config::default(), local, remote).await?; /// # Ok(()) /// # }) /// # } @@ -196,12 +197,7 @@ where /// .to_raw() /// .unwrap(); /// - /// let client = Client::with_trusted_root( - /// Config::default(), - /// &raw_root, - /// local, - /// remote, - /// ).await?; + /// let client = Client::with_trusted_root(Config::default(), &raw_root, local, remote).await?; /// # Ok(()) /// # }) /// # } @@ -218,8 +214,8 @@ where Self::new(config, tuf, local, remote).await } - /// Create a new TUF client. It will attempt to load initial root metadata from the local and remote - /// repositories using the provided keys to pin the verification. + /// Create a new TUF client. It will attempt to load initial root metadata from the local and + /// remote repositories using the provided keys to pin the verification. /// /// # Examples /// @@ -259,11 +255,13 @@ where /// let root_path = MetadataPath::root(); /// let root_version = MetadataVersion::Number(root_version); /// - /// remote.store_metadata( - /// &root_path, - /// root_version, - /// &mut root.to_raw().unwrap().as_bytes() - /// ).await?; + /// remote + /// .store_metadata( + /// &root_path, + /// root_version, + /// &mut root.to_raw().unwrap().as_bytes(), + /// ) + /// .await?; /// /// let client = Client::with_trusted_root_keys( /// Config::default(), @@ -272,7 +270,8 @@ where /// once(&public_key), /// local, /// remote, - /// ).await?; + /// ) + /// .await?; /// # Ok(()) /// # }) /// # } @@ -594,8 +593,8 @@ where // attack. On the next update cycle, begin at step 5.0 and version N of the root // metadata file. - // TODO: Consider moving the root metadata expiration check into `tuf::Database`, since that's - // where we check timestamp/snapshot/targets/delegations for expiration. + // TODO: Consider moving the root metadata expiration check into `tuf::Database`, since + // that's where we check timestamp/snapshot/targets/delegations for expiration. if tuf.trusted_root().expires() <= start_time { error!("Root metadata expired, potential freeze attack"); return Err(Error::ExpiredMetadata(MetadataPath::root())); @@ -2037,8 +2036,8 @@ mod test { assert_eq!(client.tuf.trusted_root().version(), 3); // Make sure we fetched and stored the metadata in the expected order. Note that we - // re-fetch snapshot and targets because we rotated keys, which caused `tuf::Database` to delete - // the metadata. + // re-fetch snapshot and targets because we rotated keys, which caused `tuf::Database` to + // delete the metadata. assert_eq!( client.remote_repo().take_tracks(), vec![ @@ -2415,8 +2414,8 @@ mod test { .await .unwrap(); - // Create a snapshot metadata description, and deliberately don't set the metadata length - // or hashes. + // Create a snapshot metadata description, and deliberately don't set the metadata + // length or hashes. let timestamp_description = MetadataDescription::new(1, None, HashMap::new()).unwrap(); let timestamp = diff --git a/vendor/tuf/src/crypto.rs b/vendor/tuf/src/crypto.rs index cd777d9e92..1592d9f964 100644 --- a/vendor/tuf/src/crypto.rs +++ b/vendor/tuf/src/crypto.rs @@ -76,7 +76,7 @@ fn python_tuf_compatibility_keyid_hash_algorithms() -> Option> { /// /// ``` /// use std::collections::HashMap; -/// use tuf::crypto::{retain_supported_hashes, HashValue, HashAlgorithm}; +/// use tuf::crypto::{retain_supported_hashes, HashAlgorithm, HashValue}; /// /// let mut map = HashMap::new(); /// assert!(retain_supported_hashes(&map).is_empty()); @@ -85,9 +85,7 @@ fn python_tuf_compatibility_keyid_hash_algorithms() -> Option> { /// let _ = map.insert(HashAlgorithm::Sha512, sha512_value.clone()); /// assert_eq!( /// retain_supported_hashes(&map), -/// vec![ -/// (&HashAlgorithm::Sha512, sha512_value.clone()), -/// ], +/// vec![(&HashAlgorithm::Sha512, sha512_value.clone()),], /// ); /// /// let sha256_value = HashValue::new(vec![0x02, 0x03]); @@ -223,7 +221,8 @@ fn shim_public_key( .to_string() } (_, _) => { - // We don't understand this key type and/or signature scheme, so we left it as a UTF-8 string. + // We don't understand this key type and/or signature scheme, so we left it as a UTF-8 + // string. std::str::from_utf8(public_key) .map_err(|err| { Error::Encoding(format!( diff --git a/vendor/tuf/src/database.rs b/vendor/tuf/src/database.rs index d187bc83b3..28ff73f3a6 100644 --- a/vendor/tuf/src/database.rs +++ b/vendor/tuf/src/database.rs @@ -320,7 +320,8 @@ impl Database { ///////////////////////////////////////// // TUF-1.0.5 §5.1.5: // - // Note that the expiration of the new (intermediate) root metadata file does not matter yet, because we will check for it in step 1.8. + // Note that the expiration of the new (intermediate) root metadata file does not + // matter yet, because we will check for it in step 1.8. ///////////////////////////////////////// // TUF-1.0.5 §5.1.8: @@ -370,8 +371,8 @@ impl Database { ) -> Result>> { let verified = { // FIXME(https://github.com/theupdateframework/specification/issues/113) Should we - // check if the root metadata is expired here? We do that in the other `Database::update_*` - // methods, but not here. + // check if the root metadata is expired here? We do that in the other + // `Database::update_*` methods, but not here. let trusted_root = &self.trusted_root; ///////////////////////////////////////// @@ -427,8 +428,8 @@ impl Database { // FIXME(#294): Implement this section. ///////////////////////////////////////// - // FIXME(#297): forgetting the trusted snapshot here is not part of the spec. Do we need to - // do it? + // FIXME(#297): forgetting the trusted snapshot here is not part of the spec. Do we need + // to do it? if let Some(trusted_snapshot) = &self.trusted_snapshot { if trusted_snapshot.version() != new_timestamp.snapshot().version() { diff --git a/vendor/tuf/src/error.rs b/vendor/tuf/src/error.rs index 8165f2fffb..f18f81054e 100644 --- a/vendor/tuf/src/error.rs +++ b/vendor/tuf/src/error.rs @@ -145,8 +145,8 @@ pub enum Error { new_version: u64, }, - /// The parent metadata expected the child metadata to be at one version, but was found to be at - /// another version. + /// The parent metadata expected the child metadata to be at one version, but was found to be + /// at another version. #[error("metadata {parent_role} expected metadata {child_role} version {expected_version}, but found {new_version}")] WrongMetadataVersion { /// The parent metadata that contains the child metadata's version. diff --git a/vendor/tuf/src/lib.rs b/vendor/tuf/src/lib.rs index fd492370cc..32fedcbf66 100644 --- a/vendor/tuf/src/lib.rs +++ b/vendor/tuf/src/lib.rs @@ -10,10 +10,8 @@ //! Additionally, the following two papers are valuable supplements in understanding how to //! actually implement TUF for a community repository. //! -//! - [The Diplomat paper -//! (2016)](https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/kuppusamy) -//! - [The Mercury paper -//! (2017)](https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy) +//! - [The Diplomat paper (2016)](https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/kuppusamy) +//! - [The Mercury paper (2017)](https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy) //! //! Failure to read the spec and the above papers will likely lead to an implementation that does //! not take advantage of all the security guarantees that TUF offers. diff --git a/vendor/tuf/src/metadata.rs b/vendor/tuf/src/metadata.rs index 06a1472b5b..d7d6e428fa 100644 --- a/vendor/tuf/src/metadata.rs +++ b/vendor/tuf/src/metadata.rs @@ -1036,10 +1036,14 @@ impl MetadataPath { /// # use tuf::metadata::{MetadataPath, MetadataVersion}; /// # /// let path = MetadataPath::new("foo/bar").unwrap(); - /// assert_eq!(path.components::(MetadataVersion::None), - /// ["foo".to_string(), "bar.json".to_string()]); - /// assert_eq!(path.components::(MetadataVersion::Number(1)), - /// ["foo".to_string(), "1.bar.json".to_string()]); + /// assert_eq!( + /// path.components::(MetadataVersion::None), + /// ["foo".to_string(), "bar.json".to_string()] + /// ); + /// assert_eq!( + /// path.components::(MetadataVersion::Number(1)), + /// ["foo".to_string(), "1.bar.json".to_string()] + /// ); /// ``` pub fn components(&self, version: MetadataVersion) -> Vec where @@ -1673,21 +1677,26 @@ impl TargetDescription { /// # /// let bytes: &[u8] = b"it was a pleasure to burn"; /// - /// let target_description = TargetDescription::from_slice( - /// bytes, - /// &[HashAlgorithm::Sha256, HashAlgorithm::Sha512], - /// ).unwrap(); + /// let target_description = + /// TargetDescription::from_slice(bytes, &[HashAlgorithm::Sha256, HashAlgorithm::Sha512]) + /// .unwrap(); /// /// let s = "Rd9zlbzrdWfeL7gnIEi05X-Yv2TCpy4qqZM1N72ZWQs="; /// let sha256 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); /// - /// let s ="tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ + /// let s = "tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ /// BU1zKP8GShoJuXEtCf5NkDTCEJgQ=="; /// let sha512 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); /// /// assert_eq!(target_description.length(), bytes.len() as u64); - /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha256), Some(&sha256)); - /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha512), Some(&sha512)); + /// assert_eq!( + /// target_description.hashes().get(&HashAlgorithm::Sha256), + /// Some(&sha256) + /// ); + /// assert_eq!( + /// target_description.hashes().get(&HashAlgorithm::Sha512), + /// Some(&sha512) + /// ); /// ``` pub fn from_slice(buf: &[u8], hash_algs: &[HashAlgorithm]) -> Result { Self::from_slice_with_custom(buf, hash_algs, HashMap::new()) @@ -1712,19 +1721,29 @@ impl TargetDescription { /// bytes, /// &[HashAlgorithm::Sha256, HashAlgorithm::Sha512], /// custom, - /// ).unwrap(); + /// ) + /// .unwrap(); /// /// let s = "Rd9zlbzrdWfeL7gnIEi05X-Yv2TCpy4qqZM1N72ZWQs="; /// let sha256 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); /// - /// let s ="tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ + /// let s = "tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ /// BU1zKP8GShoJuXEtCf5NkDTCEJgQ=="; /// let sha512 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); /// /// assert_eq!(target_description.length(), bytes.len() as u64); - /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha256), Some(&sha256)); - /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha512), Some(&sha512)); - /// assert_eq!(target_description.custom().get("Hello"), Some(&"World".into())); + /// assert_eq!( + /// target_description.hashes().get(&HashAlgorithm::Sha256), + /// Some(&sha256) + /// ); + /// assert_eq!( + /// target_description.hashes().get(&HashAlgorithm::Sha512), + /// Some(&sha512) + /// ); + /// assert_eq!( + /// target_description.custom().get("Hello"), + /// Some(&"World".into()) + /// ); /// ``` pub fn from_slice_with_custom( buf: &[u8], @@ -1750,21 +1769,27 @@ impl TargetDescription { /// # block_on(async { /// let bytes: &[u8] = b"it was a pleasure to burn"; /// - /// let target_description = TargetDescription::from_reader( - /// bytes, - /// &[HashAlgorithm::Sha256, HashAlgorithm::Sha512], - /// ).await.unwrap(); + /// let target_description = + /// TargetDescription::from_reader(bytes, &[HashAlgorithm::Sha256, HashAlgorithm::Sha512]) + /// .await + /// .unwrap(); /// /// let s = "Rd9zlbzrdWfeL7gnIEi05X-Yv2TCpy4qqZM1N72ZWQs="; /// let sha256 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); /// - /// let s ="tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ + /// let s = "tuIxwKybYdvJpWuUj6dubvpwhkAozWB6hMJIRzqn2jOUdtDTBg381brV4K\ /// BU1zKP8GShoJuXEtCf5NkDTCEJgQ=="; /// let sha512 = HashValue::new(BASE64URL.decode(s.as_bytes()).unwrap()); /// /// assert_eq!(target_description.length(), bytes.len() as u64); - /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha256), Some(&sha256)); - /// assert_eq!(target_description.hashes().get(&HashAlgorithm::Sha512), Some(&sha512)); + /// assert_eq!( + /// target_description.hashes().get(&HashAlgorithm::Sha256), + /// Some(&sha256) + /// ); + /// assert_eq!( + /// target_description.hashes().get(&HashAlgorithm::Sha512), + /// Some(&sha512) + /// ); /// # }) /// ``` pub async fn from_reader(read: R, hash_algs: &[HashAlgorithm]) -> Result diff --git a/vendor/tuf/src/repo_builder.rs b/vendor/tuf/src/repo_builder.rs index 77d50e2e2d..86d3004b67 100644 --- a/vendor/tuf/src/repo_builder.rs +++ b/vendor/tuf/src/repo_builder.rs @@ -393,8 +393,8 @@ where /// # ).unwrap(); /// # /// # block_on(async { - /// let mut repo = EphemeralRepository::::new(); - /// let metadata1 = RepoBuilder::create(&mut repo) + /// let mut repo = EphemeralRepository::::new(); + /// let metadata1 = RepoBuilder::create(&mut repo) /// .trusted_root_keys(&[&key]) /// .trusted_targets_keys(&[&key]) /// .trusted_snapshot_keys(&[&key]) @@ -789,8 +789,8 @@ where ) .await?; - // According to TUF section 5.5.2, when consistent snapshot is enabled, target files should be - // stored at `$HASH.FILENAME.EXT`. Otherwise it is stored at `FILENAME.EXT`. + // According to TUF section 5.5.2, when consistent snapshot is enabled, target files should + // be stored at `$HASH.FILENAME.EXT`. Otherwise it is stored at `FILENAME.EXT`. if consistent_snapshot { for digest in target_description.hashes().values() { reader.seek(SeekFrom::Start(0)).await?; @@ -1186,9 +1186,9 @@ where } } - /// Initialize a [TimestampMetadataBuilder] and pass it to the closure for further configuration. - /// This builder will then be used to generate and stage a new [TimestampMetadata] for eventual - /// commitment to the repository. + /// Initialize a [TimestampMetadataBuilder] and pass it to the closure for further + /// configuration. This builder will then be used to generate and stage a new + /// [TimestampMetadata] for eventual commitment to the repository. /// /// This builder will be initialized with: /// @@ -2410,30 +2410,28 @@ mod tests { assert!(metadata2.snapshot().is_some()); assert!(metadata2.timestamp().is_some()); - expected_metadata.extend( - vec![ - ( - (MetadataPath::targets(), MetadataVersion::Number(2)), - metadata2.targets().unwrap().as_bytes(), - ), - ( - (MetadataPath::targets(), MetadataVersion::None), - metadata2.targets().unwrap().as_bytes(), - ), - ( - (MetadataPath::snapshot(), MetadataVersion::Number(2)), - metadata2.snapshot().unwrap().as_bytes(), - ), - ( - (MetadataPath::snapshot(), MetadataVersion::None), - metadata2.snapshot().unwrap().as_bytes(), - ), - ( - (MetadataPath::timestamp(), MetadataVersion::None), - metadata2.timestamp().unwrap().as_bytes(), - ), - ], - ); + expected_metadata.extend(vec![ + ( + (MetadataPath::targets(), MetadataVersion::Number(2)), + metadata2.targets().unwrap().as_bytes(), + ), + ( + (MetadataPath::targets(), MetadataVersion::None), + metadata2.targets().unwrap().as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::Number(2)), + metadata2.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::None), + metadata2.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::timestamp(), MetadataVersion::None), + metadata2.timestamp().unwrap().as_bytes(), + ), + ]); assert_repo(&remote, &expected_metadata); @@ -2460,22 +2458,20 @@ mod tests { assert!(metadata3.snapshot().is_some()); assert!(metadata3.timestamp().is_some()); - expected_metadata.extend( - vec![ - ( - (MetadataPath::snapshot(), MetadataVersion::Number(3)), - metadata3.snapshot().unwrap().as_bytes(), - ), - ( - (MetadataPath::snapshot(), MetadataVersion::None), - metadata3.snapshot().unwrap().as_bytes(), - ), - ( - (MetadataPath::timestamp(), MetadataVersion::None), - metadata3.timestamp().unwrap().as_bytes(), - ), - ], - ); + expected_metadata.extend(vec![ + ( + (MetadataPath::snapshot(), MetadataVersion::Number(3)), + metadata3.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::snapshot(), MetadataVersion::None), + metadata3.snapshot().unwrap().as_bytes(), + ), + ( + (MetadataPath::timestamp(), MetadataVersion::None), + metadata3.timestamp().unwrap().as_bytes(), + ), + ]); assert_repo(&remote, &expected_metadata); @@ -2500,12 +2496,10 @@ mod tests { assert!(metadata4.snapshot().is_none()); assert!(metadata4.timestamp().is_some()); - expected_metadata.extend( - vec![( - (MetadataPath::timestamp(), MetadataVersion::None), - metadata4.timestamp().unwrap().as_bytes(), - )], - ); + expected_metadata.extend(vec![( + (MetadataPath::timestamp(), MetadataVersion::None), + metadata4.timestamp().unwrap().as_bytes(), + )]); assert_repo(&remote, &expected_metadata); }) diff --git a/vendor/tuf/src/repository/ephemeral.rs b/vendor/tuf/src/repository/ephemeral.rs index f4f4220ba3..88d18122e7 100644 --- a/vendor/tuf/src/repository/ephemeral.rs +++ b/vendor/tuf/src/repository/ephemeral.rs @@ -163,13 +163,9 @@ where /// Write all the metadata and targets in the [EphemeralBatchUpdate] to the source /// [EphemeralRepository] in a single batch operation. pub fn commit(self) { - self.parent_repo - .metadata - .extend(self.staging_repo.metadata); + self.parent_repo.metadata.extend(self.staging_repo.metadata); - self.parent_repo - .targets - .extend(self.staging_repo.targets); + self.parent_repo.targets.extend(self.staging_repo.targets); } } diff --git a/vendor/tuf/src/repository/http.rs b/vendor/tuf/src/repository/http.rs index 6563ed846e..bb964914ba 100644 --- a/vendor/tuf/src/repository/http.rs +++ b/vendor/tuf/src/repository/http.rs @@ -43,7 +43,9 @@ where /// Create a new repository with the given `Url` and `Client`. pub fn new(url: Url, client: Client) -> Self { HttpRepositoryBuilder { - uri: url.to_string().parse::().unwrap(), // This is dangerous, but will only exist for a short time as we migrate APIs. + uri: url.to_string().parse::().unwrap(), /* This is dangerous, but will only + * exist for a short time as we migrate + * APIs. */ client, user_agent: None, metadata_prefix: None, @@ -70,7 +72,6 @@ where /// /// Callers *should* include a custom User-Agent prefix to help maintainers of TUF repositories /// keep track of which client versions exist in the field. - /// pub fn user_agent>(mut self, user_agent: T) -> Self { self.user_agent = Some(user_agent.into()); self From 73853906495599352c6756d339625fd4d2522f64 Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 15:07:03 +0200 Subject: [PATCH 04/10] fix: address unstable_rsa clippy lints in libdd-tuf-rust These lints only surface when the `unstable_rsa` feature is enabled (the CI runs `--all-features`, but the previous local clippy run did not). - Replace three uses of the deprecated `ring::rsa::KeyPair::public_modulus_len()` with `key.public().modulus_len()` (per the deprecation notice). - Remove needless `&` borrows on two array literals passed to `Command::args()` (clippy::needless_borrows_for_generic_args). --- vendor/tuf/src/crypto.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vendor/tuf/src/crypto.rs b/vendor/tuf/src/crypto.rs index 1592d9f964..05ff32664b 100644 --- a/vendor/tuf/src/crypto.rs +++ b/vendor/tuf/src/crypto.rs @@ -624,7 +624,7 @@ impl RsaPrivateKey { /// Note: `openssl` needs to the on the `$PATH`. pub fn pkcs8() -> Result> { let gen = Command::new("openssl") - .args(&[ + .args([ "genpkey", "-algorithm", "RSA", @@ -638,7 +638,7 @@ impl RsaPrivateKey { .output()?; let mut pk8 = Command::new("openssl") - .args(&[ + .args([ "pkcs8", "-inform", "der", "-topk8", "-nocrypt", "-outform", "der", ]) .stdin(Stdio::piped()) @@ -678,10 +678,10 @@ impl RsaPrivateKey { let key = RsaKeyPair::from_pkcs8(der_key) .map_err(|_| Error::Encoding("Could not parse key as PKCS#8v2".into()))?; - if key.public_modulus_len() < 256 { + if key.public().modulus_len() < 256 { return Err(Error::IllegalArgument(format!( "RSA public modulus must be 2048 or greater. Found {}", - key.public_modulus_len() * 8 + key.public().modulus_len() * 8 ))); } @@ -703,7 +703,7 @@ impl RsaPrivateKey { impl PrivateKey for RsaPrivateKey { fn sign(&self, msg: &[u8]) -> Result { let rng = SystemRandom::new(); - let mut buf = vec![0; self.private.public_modulus_len()]; + let mut buf = vec![0; self.private.public().modulus_len()]; let scheme = match &self.public.scheme { SignatureScheme::RsaSsaPssSha256 => &RSA_PSS_SHA256, SignatureScheme::RsaSsaPssSha512 => &RSA_PSS_SHA512, From 545b8cfc05dcc28ed82acc5f62b383be050669e7 Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 15:12:33 +0200 Subject: [PATCH 05/10] chore: exclude vendor/ from licence-header check and refresh 3rd-party CSV - .github/workflows/lint.yml: add `-not -path "./vendor/*"` to the licensecheck find command, alongside the existing exclusions for symbolizer-ffi, datadog-ipc/plugins, and datadog-ipc/tarpc. Vendored code carries its own upstream licences and must not be required to carry Datadog copyright headers. - LICENSE-3rdparty.csv: regenerated with `dd-rust-license-tool dump`. The old git-sourced `tuf` entry is removed because libdd-tuf-rust is now a workspace path dependency (first-party code) and the tool correctly omits workspace members from the third-party list. --- .github/workflows/lint.yml | 2 +- LICENSE-3rdparty.csv | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 48c21e4ae7..76a1f372b9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -170,7 +170,7 @@ jobs: run: sudo apt-get install -y licensecheck - name: Check licenses # Exclude symbolizer-ffi from the checks (mostly imported code) - run: '! find . \( -name "*.rs" -o -name "*.c" -o -name "*.sh" \) -not -path "./symbolizer-ffi/*" -not -path "./datadog-ipc/plugins/*" -not -path "./datadog-ipc/tarpc/*" -print0 | xargs -0 licensecheck -c ".*" | grep -v "Apache License 2.0"' + run: '! find . \( -name "*.rs" -o -name "*.c" -o -name "*.sh" \) -not -path "./symbolizer-ffi/*" -not -path "./datadog-ipc/plugins/*" -not -path "./datadog-ipc/tarpc/*" -not -path "./vendor/*" -print0 | xargs -0 licensecheck -c ".*" | grep -v "Apache License 2.0"' license-3rdparty: needs: setup diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 7820d98ea9..16900fc61b 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -466,7 +466,6 @@ tracing-log,https://github.com/tokio-rs/tracing,MIT,Tokio Contributors tracing-subscriber,https://github.com/tokio-rs/tracing,MIT,"Eliza Weisman , David Barsky , Tokio Contributors " try-lock,https://github.com/seanmonstar/try-lock,MIT,Sean McArthur -tuf,https://github.com/theupdateframework/rust-tuf,MIT OR Apache-2.0,"heartsucker , Erick Tryzelaar " twox-hash,https://github.com/shepmaster/twox-hash,MIT,Jake Goulding typeid,https://github.com/dtolnay/typeid,MIT OR Apache-2.0,David Tolnay typenum,https://github.com/paholg/typenum,MIT OR Apache-2.0,"Paho Lurie-Gregg , Andre Bogus " From 80062d0ce6b2423090d4f302eb9a98d584621046 Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 15:15:22 +0200 Subject: [PATCH 06/10] fix: add vendor directory to codeowners --- .github/CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e410a22271..0bc59cf4ff 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -113,3 +113,7 @@ libdd-trace-utils/src/otlp_encoder/ @DataDog/apm-sdk-capabil datadog-sidecar/src/service/ffe_exposures_flusher.rs @DataDog/libdatadog-php @DataDog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk datadog-sidecar/src/service/ffe_metrics_flusher.rs @DataDog/libdatadog-php @DataDog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk .github/workflows/nix.yml @DataDog/nix-guild @DataDog/apm-common-components-core + + +# Vendored deps +vendor/tuf/ @DataDog/libdatadog-core From 03d1128d06bbcad26f07f8e7dee68c3109ad40c7 Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 15:28:32 +0200 Subject: [PATCH 07/10] fix: resolve nightly-only clippy lint in libdd-tuf-rust MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the two late-initialized `let` bindings (`targets_version`, `snapshot_version`) with a single `let (…, …) = if … { … } else { … }` expression (clippy::needless_late_init, nightly only). --- vendor/tuf/src/client.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/vendor/tuf/src/client.rs b/vendor/tuf/src/client.rs index fb4afd714f..e8e76ed5ed 100644 --- a/vendor/tuf/src/client.rs +++ b/vendor/tuf/src/client.rs @@ -1910,14 +1910,10 @@ mod test { let root_path = MetadataPath::root(); let timestamp_path = MetadataPath::timestamp(); - let targets_version; - let snapshot_version; - if consistent_snapshot { - targets_version = MetadataVersion::Number(1); - snapshot_version = MetadataVersion::Number(1); + let (targets_version, snapshot_version) = if consistent_snapshot { + (MetadataVersion::Number(1), MetadataVersion::Number(1)) } else { - targets_version = MetadataVersion::None; - snapshot_version = MetadataVersion::None; + (MetadataVersion::None, MetadataVersion::None) }; // Now, make sure that the local metadata got version 1. From 65fc9b2c62725d0c8be89ccc885fd5b7e4b1cd72 Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 15:40:50 +0200 Subject: [PATCH 08/10] test: skip libdd-tuf-rust tests under Miri MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The crate depends on ring (FFI, assembly) and async I/O helpers that are not Miri-compatible. Skipping is done at the crate level with two complementary guards: * vendor/tuf/src/lib.rs — inner attribute `#![cfg(not(all(test, miri)))]` strips the entire compilation unit when the crate is the direct Miri test target (`cfg(test)` is only set on the root crate under test, not on dependencies). This removes all `#[cfg(test)]` modules without touching each one individually. * vendor/tuf/tests/{integration,simple_example}.rs — inner attribute `#![cfg(not(miri))]` on each integration-test file. These are separate crates compiled with `cfg(test)=true` themselves, so the lib.rs guard above would not reach them. --- vendor/tuf/src/lib.rs | 6 ++++++ vendor/tuf/tests/integration.rs | 3 +++ vendor/tuf/tests/simple_example.rs | 3 +++ 3 files changed, 12 insertions(+) diff --git a/vendor/tuf/src/lib.rs b/vendor/tuf/src/lib.rs index 32fedcbf66..768c323dbd 100644 --- a/vendor/tuf/src/lib.rs +++ b/vendor/tuf/src/lib.rs @@ -1,3 +1,9 @@ +// When the crate itself is the Miri test target (cfg(test) is only set on +// the root crate under test, not on dependencies) every item in lib.rs – +// including all #[cfg(test)] modules – is stripped, so Miri finds no unit +// tests to run. Integration tests in tests/ carry their own #![cfg(not(miri))]. +#![cfg(not(all(test, miri)))] + //! This crate provides an API for talking to repositories that implement The Update Framework //! (TUF). //! diff --git a/vendor/tuf/tests/integration.rs b/vendor/tuf/tests/integration.rs index 5636d12b8d..249ab93db5 100644 --- a/vendor/tuf/tests/integration.rs +++ b/vendor/tuf/tests/integration.rs @@ -1,3 +1,6 @@ +// This crate uses ring and other libraries that are not compatible with Miri. +#![cfg(not(miri))] + use assert_matches::assert_matches; use chrono::offset::Utc; use futures_executor::block_on; diff --git a/vendor/tuf/tests/simple_example.rs b/vendor/tuf/tests/simple_example.rs index 40cdcd6010..7584b8f8c4 100644 --- a/vendor/tuf/tests/simple_example.rs +++ b/vendor/tuf/tests/simple_example.rs @@ -1,3 +1,6 @@ +// This crate uses ring and other libraries that are not compatible with Miri. +#![cfg(not(miri))] + use futures_executor::block_on; use futures_util::io::Cursor; use tuf::client::{Client, Config}; From 535a9da3098507cb758d8f39e20f61336dc05fe9 Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 16:10:41 +0200 Subject: [PATCH 09/10] fix: prevent CRLF conversion of tuf test fixtures on Windows The ECDSA canonical-JSON fixture (tests/ecdsa/ecdsa_root.canonical) embeds PEM-encoded public keys whose internal LF newlines are part of the signed bytes. Git's autocrlf on Windows converts those LFs to CRLF on checkout, producing different bytes from what the signature was computed over, causing the verify_ecdsa_signatures_against_canonical_signed_body test to panic with "BadSignature". Add a vendor/tuf/.gitattributes that marks all files under tests/ecdsa/, tests/ed25519/, and tests/rsa/ as binary so Git never touches their line endings. --- vendor/tuf/.gitattributes | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 vendor/tuf/.gitattributes diff --git a/vendor/tuf/.gitattributes b/vendor/tuf/.gitattributes new file mode 100644 index 0000000000..7115012f99 --- /dev/null +++ b/vendor/tuf/.gitattributes @@ -0,0 +1,7 @@ +# Prevent Git from converting line endings in test fixture files. +# The ECDSA canonical-JSON fixture embeds PEM newlines that are part of +# the signed content; CRLF conversion on Windows would corrupt the bytes +# and cause signature-verification test failures. +tests/ecdsa/* binary +tests/ed25519/* binary +tests/rsa/* binary From 2cc972b1f12f1e61896550f974507aedc9000196 Mon Sep 17 00:00:00 2001 From: "paul.legranddescloizeaux" Date: Tue, 18 Aug 2026 16:36:29 +0200 Subject: [PATCH 10/10] fix: resolve symlinks to their orignal contents since we picked the crate from a workspace --- libdd-remote-config/Cargo.toml | 2 +- vendor/tuf/LICENSE-APACHE | 204 ++++++++++++++++++++++++++++++++- vendor/tuf/LICENSE-MIT | 17 ++- vendor/tuf/README.md | 44 ++++++- 4 files changed, 263 insertions(+), 4 deletions(-) mode change 120000 => 100644 vendor/tuf/LICENSE-APACHE mode change 120000 => 100644 vendor/tuf/LICENSE-MIT mode change 120000 => 100644 vendor/tuf/README.md diff --git a/libdd-remote-config/Cargo.toml b/libdd-remote-config/Cargo.toml index 0a366c08d7..bbd0e67055 100644 --- a/libdd-remote-config/Cargo.toml +++ b/libdd-remote-config/Cargo.toml @@ -76,7 +76,7 @@ rand = { version = "0.8.5", optional = true } thiserror = "2" hashbrown = "0.15" # branch = "opw-develop" -tuf = { package = "libdd-tuf-rust", path = "../vendor/tuf", default-features = false, optional = true } +tuf = { package = "libdd-tuf-rust", path = "../vendor/tuf", version = "0.3.0-beta10", default-features = false, optional = true } chrono = { version = "0.4", default-features = false, features = [ "clock", ], optional = true } diff --git a/vendor/tuf/LICENSE-APACHE b/vendor/tuf/LICENSE-APACHE deleted file mode 120000 index 965b606f33..0000000000 --- a/vendor/tuf/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-APACHE \ No newline at end of file diff --git a/vendor/tuf/LICENSE-APACHE b/vendor/tuf/LICENSE-APACHE new file mode 100644 index 0000000000..e646da58f1 --- /dev/null +++ b/vendor/tuf/LICENSE-APACHE @@ -0,0 +1,203 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ +Copyright (c) 2017 heartsucker, Advanced Telematic Systems GmbH + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/vendor/tuf/LICENSE-MIT b/vendor/tuf/LICENSE-MIT deleted file mode 120000 index 76219eb72e..0000000000 --- a/vendor/tuf/LICENSE-MIT +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-MIT \ No newline at end of file diff --git a/vendor/tuf/LICENSE-MIT b/vendor/tuf/LICENSE-MIT new file mode 100644 index 0000000000..045d2f0b7a --- /dev/null +++ b/vendor/tuf/LICENSE-MIT @@ -0,0 +1,16 @@ +The MIT License (MIT) + +Copyright (c) 2017 heartsucker, Advanced Telematic Systems GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/tuf/README.md b/vendor/tuf/README.md deleted file mode 120000 index 32d46ee883..0000000000 --- a/vendor/tuf/README.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/vendor/tuf/README.md b/vendor/tuf/README.md new file mode 100644 index 0000000000..59960a63db --- /dev/null +++ b/vendor/tuf/README.md @@ -0,0 +1,43 @@ +# libdd-tuf-rust + +> ⚠️ **This is a vendored fork of [Datadog/rust-tuf](https://github.com/DataDog/rust-tuf), +> itself a fork of [theupdateframework/rust-tuf](https://github.com/theupdateframework/rust-tuf).** +> +> It is published under the name `libdd-tuf-rust` to allow libdatadog to be released on +> crates.io without git dependencies. **Do not use this crate directly** — it is an +> internal implementation detail of the libdatadog workspace. +> +> To update this vendored copy, copy the sources from the Datadog fork at the commit +> used by libdatadog. + +--- + +## Original upstream README + +# rust-tuf + +A Rust implementation of [The Update Framework (TUF)](https://theupdateframework.github.io/). + +Full documentation is hosted at [docs.rs](https://docs.rs/crate/tuf). + +## Warning: Beta Software + +This is under active development and may not suitable for production use. Further, +the API is unstable and you should be prepared to refactor on even patch releases. + +## Contributing + +Please make all pull requests to the `develop` branch. + +### Bugs + +This project has a **full disclosure** policy on security related errors. Please +treat these errors like all other bugs and file a public issue. Errors communicated +via other channels will be immediately made public. + +## Legal + +### License + +This work is dual licensed under the MIT and Apache-2.0 licenses. +See [LICENSE-MIT](./LICENSE-MIT) and [LICENSE-APACHE](./LICENSE-APACHE) for details.