diff --git a/Cargo.lock b/Cargo.lock index bb490f17..abfc6765 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -651,6 +662,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bindgen" version = "0.69.5" @@ -695,6 +712,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "bmrng" version = "0.4.0" @@ -754,6 +780,15 @@ dependencies = [ "either", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cbor-diag" version = "0.1.12" @@ -839,6 +874,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -905,6 +950,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -986,6 +1037,17 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1635,6 +1697,16 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -2033,6 +2105,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem" version = "3.0.4" @@ -2043,6 +2125,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2081,6 +2172,33 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.31" @@ -2598,6 +2716,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2622,6 +2749,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -2832,6 +2970,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3136,6 +3284,7 @@ dependencies = [ "async-recursion", "async-trait", "aws-lc-rs", + "base64 0.22.1", "bytes", "chrono", "dyn-clone", @@ -3151,6 +3300,7 @@ dependencies = [ "olpc-cjson", "pem", "percent-encoding", + "pkcs8", "reqwest 0.12.8", "rustls 0.23.14", "serde", diff --git a/tough-ssm/src/lib.rs b/tough-ssm/src/lib.rs index 81356a6a..735b2887 100644 --- a/tough-ssm/src/lib.rs +++ b/tough-ssm/src/lib.rs @@ -48,7 +48,7 @@ impl KeySource for SsmKeySource { })? .as_bytes() .to_vec(); - let sign = Box::new(parse_keypair(&data).context(error::KeyPairParseSnafu)?); + let sign = Box::new(parse_keypair(&data, None).context(error::KeyPairParseSnafu)?); Ok(sign) } diff --git a/tough/Cargo.toml b/tough/Cargo.toml index 4d934c99..cd62ae6b 100644 --- a/tough/Cargo.toml +++ b/tough/Cargo.toml @@ -12,6 +12,7 @@ edition = "2018" async-recursion = "1" async-trait = "0.1" aws-lc-rs = "1" +base64 = "0.22" bytes = "1" chrono = { version = "0.4", default-features = false, features = ["std", "alloc", "serde", "clock"] } dyn-clone = "1" @@ -23,6 +24,7 @@ log = "0.4" olpc-cjson = { version = "0.1", path = "../olpc-cjson" } pem = "3" percent-encoding = "2" +pkcs8 = { version = "0.10", features = ["encryption", "pem", "std"] } reqwest = { version = "0.12", optional = true, default-features = false, features = ["stream"] } rustls = "0.23" serde = { version = "1", features = ["derive"] } diff --git a/tough/src/editor/test.rs b/tough/src/editor/test.rs index 9d76e5bb..1c122dd4 100644 --- a/tough/src/editor/test.rs +++ b/tough/src/editor/test.rs @@ -54,7 +54,10 @@ mod tests { #[tokio::test] async fn empty_repository() { let root_key = key_path(); - let key_source = LocalKeySource { path: root_key }; + let key_source = LocalKeySource { + path: root_key, + password: None, + }; let root_path = root_path(); let editor = RepositoryEditor::new(root_path).await.unwrap(); @@ -112,7 +115,10 @@ mod tests { async fn complete_repository() { let root = root_path(); let root_key = key_path(); - let key_source = LocalKeySource { path: root_key }; + let key_source = LocalKeySource { + path: root_key, + password: None, + }; let timestamp_expiration = Utc::now().checked_add_signed(days(3)).unwrap(); let timestamp_version = NonZeroU64::new(1234).unwrap(); let snapshot_expiration = Utc::now().checked_add_signed(days(21)).unwrap(); diff --git a/tough/src/key_source.rs b/tough/src/key_source.rs index b556f5fb..e6665f1b 100644 --- a/tough/src/key_source.rs +++ b/tough/src/key_source.rs @@ -33,6 +33,8 @@ pub trait KeySource: Debug + Send + Sync { pub struct LocalKeySource { /// The path to a local key file in PEM pkcs8 or RSA format. pub path: PathBuf, + /// Optional password for the key file. + pub password: Option, } /// Implements the `KeySource` trait for a `LocalKeySource` (file) @@ -44,7 +46,8 @@ impl KeySource for LocalKeySource { let data = tokio::fs::read(&self.path) .await .context(error::FileReadSnafu { path: &self.path })?; - Ok(Box::new(parse_keypair(&data)?)) + let password: Option<&str> = self.password.as_deref(); + Ok(Box::new(parse_keypair(&data, password)?)) } async fn write( diff --git a/tough/src/sign.rs b/tough/src/sign.rs index 47810095..78777440 100644 --- a/tough/src/sign.rs +++ b/tough/src/sign.rs @@ -11,10 +11,12 @@ use crate::sign::SignKeyPair::RSA; use async_trait::async_trait; use aws_lc_rs::rand::SecureRandom; use aws_lc_rs::signature::{EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaKeyPair}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use pkcs8::der::Decode; use snafu::ResultExt; use std::collections::HashMap; use std::error::Error; - +use std::str; /// This trait must be implemented for each type of key with which you will /// sign things. #[async_trait] @@ -166,17 +168,44 @@ impl Sign for SignKeyPair { } } +/// Decrypts an RSA private key in PEM format using the given password. +/// Returns the decrypted key in PKCS8 format +pub fn decrypt_key( + encrypted_key: &[u8], + password: &str, +) -> std::result::Result, Box> { + let pem_str = std::str::from_utf8(encrypted_key)?; + let pem = pem::parse(pem_str)?; + let encrypted_private_key_document = pkcs8::EncryptedPrivateKeyInfo::from_der(pem.contents())?; + let decrypted_private_key_document = + encrypted_private_key_document.decrypt(password.as_bytes())?; + let decrypted_key_bytes: Vec = decrypted_private_key_document.as_bytes().to_vec(); + let decrypted_key_base64 = STANDARD.encode(decrypted_key_bytes); + let pem_key = + format!("-----BEGIN PRIVATE KEY-----\n{decrypted_key_base64}\n-----END PRIVATE KEY-----"); + let pem_key_bytes = pem_key.as_bytes().to_vec(); + Ok(pem_key_bytes) +} + /// Parses a supplied keypair and if it is recognized, returns an object that /// implements the Sign trait /// Accepted Keys: ED25519 pkcs8, Ecdsa pkcs8, RSA -pub fn parse_keypair(key: &[u8]) -> Result { - if let Ok(ed25519_key_pair) = Ed25519KeyPair::from_pkcs8(key) { +pub fn parse_keypair(key: &[u8], password: Option<&str>) -> Result { + let decrypted_key = if let Some(pw) = password { + decrypt_key(key, pw).unwrap_or_else(|_| key.to_vec()) + } else { + key.to_vec() + }; + let decrypted_key_slice: &[u8] = &decrypted_key; + + if let Ok(ed25519_key_pair) = Ed25519KeyPair::from_pkcs8(decrypted_key_slice) { Ok(SignKeyPair::ED25519(ed25519_key_pair)) - } else if let Ok(ecdsa_key_pair) = - EcdsaKeyPair::from_pkcs8(&aws_lc_rs::signature::ECDSA_P256_SHA256_ASN1_SIGNING, key) - { + } else if let Ok(ecdsa_key_pair) = EcdsaKeyPair::from_pkcs8( + &aws_lc_rs::signature::ECDSA_P256_SHA256_ASN1_SIGNING, + decrypted_key_slice, + ) { Ok(SignKeyPair::ECDSA(ecdsa_key_pair)) - } else if let Ok(pem) = pem::parse(key) { + } else if let Ok(pem) = pem::parse(decrypted_key_slice) { match pem.tag() { "PRIVATE KEY" => { if let Ok(rsa_key_pair) = RsaKeyPair::from_pkcs8(pem.contents()) { diff --git a/tough/tests/data/snakeoil_3.pem b/tough/tests/data/snakeoil_3.pem new file mode 100644 index 00000000..1804f2e4 --- /dev/null +++ b/tough/tests/data/snakeoil_3.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQ1pf1TT8mQ8GE9jVr +QO+8EQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEN6HKACxvXCp4O64 +CCMBrP8EggTQY3hoy+06BWyDczEtxHNBe9Wdu2AYU4x05QyieGS33Ik3ASDAtHPf +t4Ao1SdttYcsNCYSX5gccfTsWdiLN3BzyNhus6JQzQ2CTAjd6gGcD6Qcnu21C/ii +Ds3zzSkUz5pSsKDlgjdkDfm4cp57Zg7YeMf/YEvo056kYgMy1Yyc9tT8tzqX3oP9 +Wpte0dlsf5DFVVosfKDHATeV0fQKODN+M2zFZAixgheNDRH4MfnY8SjCIux+11Ud +ZZ5C4DXuQdedlxFxR5GaHGMPmYpcXg1WiT3lT+tzcieko4B/vUPjlLEcL8cyg7eQ +IWSkuhGgKKvuJs52R3jJeOJTQh3Jo661eny7A+xUyGZ0YiujFSFaD5zzYZDtszrS +oenmIZQ+5cqi18ObRgk53wSI/tnh18ZJ+PN3WjJyhvxqMVo+7EzI0NallYMEE/iP +I5PZjF/Bf7/bqasbGQmVhzHy+v8v97oF0ZRa6vwzWJos7vAbQoMPNHWGaffFFgZK +nlxLgdz1bshk7ODWkZ0CNJVGxt1Ojk3ATyW4O91W8UKc4has2FUQqpHtZjzju7dg +zM/czYDwmS5ZWNR7mPSjBkH9r14MRVKnA60lKI+0JUxXdpu4j8O/YqX20aiToRgq +D3qLabAKYVSe3hW9m7sqgGb7T6PZjl99hHu5K6ljzu9XDg4eXdDQPdNeTJATOW1z +X1QYZuA1Je68CSDnN4QMibrCPlkFwxHY2jBbPuxvdKsZMh3mS+mqdB/hBNq6n+LZ +sxIYcJ+o4t/5a6nvgMWGv2EVJIe4vMwWPYGh/4LqcwStP3/RFz7lhXFL9QzcqmlP +8voNFvek6Aaogf32S8RHhjjS77QwU34QE7+Y93v9OuvPyqamW5xHUjC0W5rZMo18 +/B10n+O6EySGaaMkTcCw/hkBLhZ0xkDAMGFeEa89T4hEzL+ZQzGLeB7Rl26sEiqb +JtWtaBN0TVd7bSe5aRk4LFzlRfG5SZC681JZZOll6jq9DWkaJPwe1C+hvYyuXZlk +0SePeL4lJBYelvAVX5EJCI+Nbsb/B6pICW9cI+pyiGxU6YVnzy00fMEFKH/fhT1g +WSOoQh4cPF453ll4DRP9Nwae7LdtiHrCqmReoq3xxTgyf8eJstzDZgivS6M2cvPf +69vDensXC6rkkJF/8Lr4uuyYwr2Zi1/bx+qN88dtl7OuZiCJ/MCvpdeXzE6uHP9C +TE+zlRdW8cyUie6dg5huHkCUBBTBKegj9Qtw+6y91rBvOryiVACCu9FawdCrOpP9 +aRu7tU2w1o5RL/jMLtrCljABChNk5US3Iq7SOBlPn8Qu9OBDloW964K66dFcou6U +c1GCFlh25aBv3MxbZ0EuoO5ZRfCTFLBMpJ2aKh+W2Twiw0mrOwMaU3mGBMyLX64p +Q3p+tM5zHPXG4+lGA+dGASuo2ohLY350gwlZuaZUJLck7H0JZwgnKIp5z0Na8Suu +YTlt8TYqZJTM1LM1RyQltD2iWHTWcOfb2ZfE4l4K5wl7wa7bBYjGC65MoAOTpHKh +kQ/sYfZYP9+1Ffwm8Wj0FDkwhDIxotAVeWNNzmRr2EAuMEdJ4wqjjELDzfZ7q7d8 +hxxy9bSKcUS/AxmhqIAA4u+jDJpD3tUTI+LCao0HxDrnN7ljNjEx1kk= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tough/tests/repo_editor.rs b/tough/tests/repo_editor.rs index ba0972a6..0ce6209a 100644 --- a/tough/tests/repo_editor.rs +++ b/tough/tests/repo_editor.rs @@ -153,14 +153,19 @@ async fn create_sign_write_reload_repo() { .unwrap(); let targets_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = - &[Box::new(LocalKeySource { path: key_path() })]; + &[Box::new(LocalKeySource { + path: key_path(), + password: None, + })]; let role1_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = &[Box::new(LocalKeySource { path: targets_key_path(), + password: None, })]; let role2_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = &[Box::new(LocalKeySource { path: targets_key_path1(), + password: None, })]; // add role1 to targets @@ -257,14 +262,19 @@ async fn create_role_flow() { let editor = test_repo_editor().await; let targets_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = - &[Box::new(LocalKeySource { path: key_path() })]; + &[Box::new(LocalKeySource { + path: key_path(), + password: None, + })]; let role1_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = &[Box::new(LocalKeySource { path: targets_key_path(), + password: None, })]; let role2_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = &[Box::new(LocalKeySource { path: targets_key_path1(), + password: None, })]; // write the repo to temp location @@ -320,7 +330,10 @@ async fn create_role_flow() { //sign everything since targets key is the same as snapshot and timestamp let root_key = key_path(); - let key_source = LocalKeySource { path: root_key }; + let key_source = LocalKeySource { + path: root_key, + password: None, + }; let timestamp_expiration = Utc::now().checked_add_signed(days(3)).unwrap(); let timestamp_version = NonZeroU64::new(1234).unwrap(); let snapshot_expiration = Utc::now().checked_add_signed(days(21)).unwrap(); @@ -430,7 +443,10 @@ async fn create_role_flow() { let metadata_base_url_out = dir_url(&metadata_destination_out); // add outdir to repo let root_key = key_path(); - let key_source = LocalKeySource { path: root_key }; + let key_source = LocalKeySource { + path: root_key, + password: None, + }; let mut editor = RepositoryEditor::from_repo(root_path(), new_repo) .await @@ -481,14 +497,19 @@ async fn update_targets_flow() { let editor = test_repo_editor().await; let targets_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = - &[Box::new(LocalKeySource { path: key_path() })]; + &[Box::new(LocalKeySource { + path: key_path(), + password: None, + })]; let role1_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = &[Box::new(LocalKeySource { path: targets_key_path(), + password: None, })]; let role2_key: &[std::boxed::Box<(dyn tough::key_source::KeySource + 'static)>] = &[Box::new(LocalKeySource { path: targets_key_path1(), + password: None, })]; // write the repo to temp location @@ -544,7 +565,10 @@ async fn update_targets_flow() { //sign everything since targets key is the same as snapshot and timestamp let root_key = key_path(); - let key_source = LocalKeySource { path: root_key }; + let key_source = LocalKeySource { + path: root_key, + password: None, + }; let timestamp_expiration = Utc::now().checked_add_signed(days(3)).unwrap(); let timestamp_version = NonZeroU64::new(1234).unwrap(); let snapshot_expiration = Utc::now().checked_add_signed(days(21)).unwrap(); @@ -654,7 +678,10 @@ async fn update_targets_flow() { let metadata_base_url_out = dir_url(&metadata_destination_out); // add outdir to repo let root_key = key_path(); - let key_source = LocalKeySource { path: root_key }; + let key_source = LocalKeySource { + path: root_key, + password: None, + }; let mut editor = RepositoryEditor::from_repo(root_path(), new_repo) .await diff --git a/tough/tests/target_path_safety.rs b/tough/tests/target_path_safety.rs index 12140c79..1441090e 100644 --- a/tough/tests/target_path_safety.rs +++ b/tough/tests/target_path_safety.rs @@ -26,6 +26,7 @@ fn later() -> DateTime { async fn create_root(root_path: &Path, consistent_snapshot: bool) -> Vec> { let keys: Vec> = vec![Box::new(LocalKeySource { path: test_data().join("snakeoil.pem"), + password: None, })]; let key_pair = keys.first().unwrap().as_sign().await.unwrap().tuf_key(); diff --git a/tuftool/src/add_key_role.rs b/tuftool/src/add_key_role.rs index 35d6e295..2bf81fef 100644 --- a/tuftool/src/add_key_role.rs +++ b/tuftool/src/add_key_role.rs @@ -29,10 +29,18 @@ pub(crate) struct AddKeyArgs { #[arg(short, long = "key", required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files to sign with + #[arg(short, long = "password")] + passwords: Option>, + /// New keys to be used for role #[arg(long = "new-key", required = true)] new_keys: Vec, + /// [Optional] passwords/passphrases of the new keys + #[arg(long = "new-password")] + new_passwords: Option>, + /// TUF repository metadata base URL #[arg(short, long = "metadata-url")] metadata_base_url: Url, @@ -66,8 +74,18 @@ impl AddKeyArgs { async fn add_key(&self, role: &str, mut editor: TargetsEditor) -> Result<()> { // create the keypairs to add let mut key_pairs = HashMap::new(); - for source in &self.new_keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let new_passwords = match &self.new_passwords { + Some(pws) => pws, + None => &vec![], + }; + + if new_passwords.len() > self.new_keys.len() { + error::MoreNewPasswordsSnafu.fail()?; + } + for (i, source) in self.new_keys.iter().enumerate() { + let password = new_passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; let key_pair = key_source .as_sign() .await @@ -83,8 +101,17 @@ impl AddKeyArgs { } let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/src/add_role.rs b/tuftool/src/add_role.rs index a097fe73..cbad4714 100644 --- a/tuftool/src/add_role.rs +++ b/tuftool/src/add_role.rs @@ -33,6 +33,10 @@ pub(crate) struct AddRoleArgs { #[arg(short, long = "key", required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(long = "password")] + passwords: Option>, + /// TUF repository metadata base URL #[arg(short, long = "metadata-url")] metadata_base_url: Url, @@ -120,8 +124,17 @@ impl AddRoleArgs { }; let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } @@ -155,8 +168,17 @@ impl AddRoleArgs { /// Adds a role to metadata using repo Editor async fn with_repo_editor(&self, role: &str, mut editor: RepositoryEditor) -> Result<()> { let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/src/create.rs b/tuftool/src/create.rs index 586f29e5..a72a439d 100644 --- a/tuftool/src/create.rs +++ b/tuftool/src/create.rs @@ -31,6 +31,10 @@ pub(crate) struct CreateArgs { #[arg(short, long = "key", required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(short, long = "password")] + passwords: Option>, + /// The directory where the repository will be written #[arg(short, long)] outdir: PathBuf, @@ -80,8 +84,17 @@ pub(crate) struct CreateArgs { impl CreateArgs { pub(crate) async fn run(&self) -> Result<()> { let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/src/create_role.rs b/tuftool/src/create_role.rs index 9b7f32ca..956beba4 100644 --- a/tuftool/src/create_role.rs +++ b/tuftool/src/create_role.rs @@ -27,6 +27,10 @@ pub(crate) struct CreateRoleArgs { #[arg(short, long, required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(short, long = "password")] + passwords: Option>, + /// The directory where the repository will be written #[arg(short, long)] outdir: PathBuf, @@ -39,8 +43,18 @@ pub(crate) struct CreateRoleArgs { impl CreateRoleArgs { pub(crate) async fn run(&self, role: &str) -> Result<()> { let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/src/error.rs b/tuftool/src/error.rs index b2f89af0..663c850c 100644 --- a/tuftool/src/error.rs +++ b/tuftool/src/error.rs @@ -356,6 +356,12 @@ pub(crate) enum Error { source: tokio::task::JoinError, backtrace: Backtrace, }, + + #[snafu(display("More passwords provided than key sources"))] + MorePasswords { backtrace: Backtrace }, + + #[snafu(display("More new passwords provided than new key sources"))] + MoreNewPasswords { backtrace: Backtrace }, } // Extracts the status code from a reqwest::Error and converts it to a string to be displayed diff --git a/tuftool/src/remove_key_role.rs b/tuftool/src/remove_key_role.rs index abd3fb6d..2f8bf120 100644 --- a/tuftool/src/remove_key_role.rs +++ b/tuftool/src/remove_key_role.rs @@ -29,6 +29,10 @@ pub(crate) struct RemoveKeyArgs { #[arg(short, long = "key", required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(short, long = "password")] + passwords: Option>, + /// Key to be removed will look similar to `8ec3a843a0f9328c863cac4046ab1cacbbc67888476ac7acf73d9bcd9a223ada` #[arg(long = "keyid", required = true)] remove: Decoded, @@ -64,8 +68,18 @@ impl RemoveKeyArgs { /// Removes keys from a delegated role using targets Editor async fn remove_key(&self, role: &str, mut editor: TargetsEditor) -> Result<()> { let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/src/remove_role.rs b/tuftool/src/remove_role.rs index 1856a0fe..42cd9db0 100644 --- a/tuftool/src/remove_role.rs +++ b/tuftool/src/remove_role.rs @@ -28,6 +28,10 @@ pub(crate) struct RemoveRoleArgs { #[arg(short, long = "key", required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(short, long = "password")] + passwords: Option>, + /// TUF repository metadata base URL #[arg(short, long = "metadata-url")] metadata_base_url: Url, @@ -63,8 +67,17 @@ impl RemoveRoleArgs { /// Removes a delegated role from a `Targets` role using `TargetsEditor` async fn remove_delegated_role(&self, role: &str, mut editor: TargetsEditor) -> Result<()> { let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/src/root.rs b/tuftool/src/root.rs index fc2d8d70..65144b12 100644 --- a/tuftool/src/root.rs +++ b/tuftool/src/root.rs @@ -31,6 +31,9 @@ pub(crate) enum Command { /// The new key to be added #[arg(short, long = "key")] key_source: Vec, + /// [Optional] password of the new key to be added + #[arg(short, long = "password")] + password: Option>, /// The role to add the key to #[arg(short, long = "role")] roles: Vec, @@ -65,6 +68,9 @@ pub(crate) enum Command { /// The role to add the key to #[arg(short, long = "role")] roles: Vec, + /// [Optional] password/passphrase of the new key + #[arg(short, long = "password")] + password: Option, }, /// Create a new root.json metadata file Init { @@ -107,6 +113,9 @@ pub(crate) enum Command { /// Key source(s) to sign the file with #[arg(short, long = "key")] key_sources: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(short, long = "password")] + passwords: Option>, /// Optional - Path of older root.json that contains the key-id #[arg(short, long)] cross_sign: Option, @@ -147,7 +156,8 @@ impl Command { path, roles, key_source, - } => Command::add_key(&path, &roles, &key_source).await, + password, + } => Command::add_key(&path, &roles, &key_source, &password).await, Command::RemoveKey { path, key_id, role } => { Command::remove_key(&path, &key_id, role).await } @@ -157,16 +167,24 @@ impl Command { key_source, bits, exponent, - } => Command::gen_rsa_key(&path, &roles, &key_source, bits, exponent).await, + password, + } => Command::gen_rsa_key(&path, &roles, &key_source, bits, exponent, password).await, Command::Sign { path, key_sources, + passwords, cross_sign, ignore_threshold, } => { let mut keys = Vec::new(); - for source in &key_sources { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = passwords.unwrap_or_default(); + if passwords.len() > key_sources.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in key_sources.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } Command::sign(&path, &keys, cross_sign, ignore_threshold).await @@ -239,10 +257,24 @@ impl Command { } #[allow(clippy::borrowed_box)] - async fn add_key(path: &Path, roles: &[RoleType], key_source: &Vec) -> Result<()> { + async fn add_key( + path: &Path, + roles: &[RoleType], + key_source: &[String], + password: &Option>, + ) -> Result<()> { let mut keys = Vec::new(); - for source in key_source { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match password { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > key_source.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in key_source.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } let mut root: Signed = load_file(path).await?; @@ -292,6 +324,7 @@ impl Command { key_source: &str, bits: u16, exponent: u32, + password: Option, ) -> Result<()> { let mut root: Signed = load_file(path).await?; @@ -303,6 +336,10 @@ impl Command { command.arg("-pkeyopt"); command.arg(format!("rsa_keygen_pubexp:{exponent}")); + if let Some(password_str) = password.as_deref() { + command.args(["-aes256", "-pass"]); + command.arg(format!("pass:{password_str}")); + } let command_str = format!("{command:?}"); let output = command.output().context(error::CommandExecSnafu { command_str: &command_str, @@ -317,9 +354,10 @@ impl Command { let stdout = String::from_utf8(output.stdout).context(error::CommandUtf8Snafu { command_str })?; - let key_pair = parse_keypair(stdout.as_bytes()).context(error::KeyPairParseSnafu)?; + let key_pair = parse_keypair(stdout.as_bytes(), password.as_deref()) + .context(error::KeyPairParseSnafu)?; let key_id = hex::encode(add_key(&mut root.signed, roles, key_pair.tuf_key())?); - let key = parse_key_source(key_source)?; + let key = parse_key_source(key_source, None)?; key.write(&stdout, &key_id) .await .context(error::WriteKeySourceSnafu)?; diff --git a/tuftool/src/source.rs b/tuftool/src/source.rs index 16c6c8d3..b146c46f 100644 --- a/tuftool/src/source.rs +++ b/tuftool/src/source.rs @@ -49,10 +49,13 @@ use url::Url; /// Users are welcome to add their own sources of keys by implementing /// the `KeySource` trait in the `tough` library. A user can then add /// to this parser to support them in `tuftool`. -pub(crate) fn parse_key_source(input: &str) -> Result> { +pub(crate) fn parse_key_source( + input: &str, + password: Option, +) -> Result> { let path_or_url = parse_path_or_url(input)?; match path_or_url { - PathOrUrl::Path(path) => Ok(Box::new(LocalKeySource { path })), + PathOrUrl::Path(path) => Ok(Box::new(LocalKeySource { path, password })), PathOrUrl::Url(url) => { match url.scheme() { #[cfg(any(feature = "aws-sdk-rust", feature = "aws-sdk-rust-rustls"))] diff --git a/tuftool/src/transfer_metadata.rs b/tuftool/src/transfer_metadata.rs index 3590c812..818b41c2 100644 --- a/tuftool/src/transfer_metadata.rs +++ b/tuftool/src/transfer_metadata.rs @@ -23,6 +23,10 @@ pub(crate) struct TransferMetadataArgs { #[arg(short, long = "key", required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(short, long = "password")] + passwords: Option>, + /// TUF repository metadata base URL #[arg(short, long = "metadata-url")] metadata_base_url: Url, @@ -82,8 +86,17 @@ WARNING: `--allow-expired-repo` was passed; this is unsafe and will not establis impl TransferMetadataArgs { pub(crate) async fn run(&self) -> Result<()> { let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/src/update.rs b/tuftool/src/update.rs index c3c7b76a..0b16d0f7 100644 --- a/tuftool/src/update.rs +++ b/tuftool/src/update.rs @@ -42,6 +42,10 @@ pub(crate) struct UpdateArgs { #[arg(short, long = "key", required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(short, long = "password")] + passwords: Option>, + /// TUF repository metadata base URL #[arg(short, long = "metadata-url")] metadata_base_url: Url, @@ -135,8 +139,17 @@ impl UpdateArgs { async fn update_metadata(&self, mut editor: RepositoryEditor) -> Result<()> { let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/src/update_targets.rs b/tuftool/src/update_targets.rs index 3221a619..08c049c6 100644 --- a/tuftool/src/update_targets.rs +++ b/tuftool/src/update_targets.rs @@ -39,6 +39,10 @@ pub(crate) struct UpdateTargetsArgs { #[arg(short, long = "key", required = true)] keys: Vec, + /// [Optional] passwords/passphrases of the Key files + #[arg(short, long = "password")] + passwords: Option>, + /// TUF repository metadata base URL #[arg(short, long = "metadata-url")] metadata_base_url: Url, @@ -78,8 +82,17 @@ impl UpdateTargetsArgs { async fn update_targets(&self, mut editor: TargetsEditor) -> Result<()> { let mut keys = Vec::new(); - for source in &self.keys { - let key_source = parse_key_source(source)?; + let default_password = String::new(); + let passwords = match &self.passwords { + Some(pws) => pws, + None => &vec![], + }; + if passwords.len() > self.keys.len() { + error::MorePasswordsSnafu.fail()?; + } + for (i, source) in self.keys.iter().enumerate() { + let password = passwords.get(i).unwrap_or(&default_password); + let key_source = parse_key_source(source, Some(password.to_string()))?; keys.push(key_source); } diff --git a/tuftool/tests/root_command.rs b/tuftool/tests/root_command.rs index 288a08c5..8d71a11b 100644 --- a/tuftool/tests/root_command.rs +++ b/tuftool/tests/root_command.rs @@ -43,68 +43,87 @@ fn initialize_root_json(root_json: &str) { .success(); } -fn add_key_root(keys: &Vec<&str>, root_json: &str) { +fn add_key_root(keys: &Vec<&str>, root_json: &str, password: Option<&str>) { let mut cmd = Command::cargo_bin("tuftool").unwrap(); - cmd.args(["root", "add-key", root_json, "--role", "root"]); for key in keys { cmd.args(["-k", key]); } - + if let Some(pass) = password { + if !pass.is_empty() { + cmd.args(["--password", pass]); + } + } cmd.assert().success(); } -fn add_key_timestamp(key: &str, root_json: &str) { - Command::cargo_bin("tuftool") - .unwrap() - .args([ - "root", - "add-key", - root_json, - "-k", - key, - "--role", - "timestamp", - ]) - .assert() - .success(); +fn add_key_timestamp(key: &str, root_json: &str, password: Option<&str>) { + let mut cmd = Command::cargo_bin("tuftool").unwrap(); + cmd.args([ + "root", + "add-key", + root_json, + "-k", + key, + "--role", + "timestamp", + ]); + + if let Some(pass) = password { + if !pass.is_empty() { + cmd.args(["--password", pass]); + } + } + cmd.assert().success(); } -fn add_key_snapshot(key: &str, root_json: &str) { - Command::cargo_bin("tuftool") - .unwrap() - .args([ - "root", "add-key", root_json, "-k", key, "--role", "snapshot", - ]) - .assert() - .success(); +fn add_key_snapshot(key: &str, root_json: &str, password: Option<&str>) { + let mut cmd = Command::cargo_bin("tuftool").unwrap(); + cmd.args([ + "root", "add-key", root_json, "-k", key, "--role", "snapshot", + ]); + + if let Some(pass) = password { + if !pass.is_empty() { + cmd.args(["--password", pass]); + } + } + cmd.assert().success(); } -fn add_key_targets(key: &str, root_json: &str) { - Command::cargo_bin("tuftool") - .unwrap() - .args(["root", "add-key", root_json, "-k", key, "--role", "targets"]) - .assert() - .success(); +fn add_key_targets(key: &str, root_json: &str, password: Option<&str>) { + let mut cmd = Command::cargo_bin("tuftool").unwrap(); + cmd.args(["root", "add-key", root_json, "-k", key, "--role", "targets"]); + + if let Some(pass) = password { + if !pass.is_empty() { + cmd.args(["--password", pass]); + } + } + cmd.assert().success(); } fn add_keys_all_roles(keys: Vec<&str>, root_json: &str) { - add_key_root(&keys, root_json); + add_key_root(&keys, root_json, None); // Only add the first key for the rest until we have tests that want it for all keys let key = keys.first().unwrap(); - add_key_timestamp(key, root_json); - add_key_snapshot(key, root_json); - add_key_targets(key, root_json); + add_key_timestamp(key, root_json, None); + add_key_snapshot(key, root_json, None); + add_key_targets(key, root_json, None); } -fn sign_root_json(key: &str, root_json: &str) { - Command::cargo_bin("tuftool") - .unwrap() - // We don't have enough signatures to meet the threshold, so we have to pass `-i` - .args(["root", "sign", root_json, "-i", "-k", key]) - .assert() - .success(); +fn sign_root_json(key: &str, root_json: &str, password: Option<&str>) { + let mut cmd = Command::cargo_bin("tuftool").unwrap(); + // We don't have enough signatures to meet the threshold, so we have to pass `-i` + cmd.args(["root", "sign", root_json, "-i", "-k", key]); + + if let Some(pass) = password { + if !pass.is_empty() { + cmd.args(["--password", pass]); + } + } + cmd.assert().success(); } fn sign_root_json_failure(key: &str, root_json: &str) { @@ -174,7 +193,11 @@ fn create_root() { // Add keys for all roles add_keys_all_roles(vec![key_1.to_str().unwrap()], root_json.to_str().unwrap()); // Add second key for root role - add_key_root(&vec![key_2.to_str().unwrap()], root_json.to_str().unwrap()); + add_key_root( + &vec![key_2.to_str().unwrap()], + root_json.to_str().unwrap(), + None, + ); // Sign root.json with 1 key sign_root_json_two_keys( key_1.to_str().unwrap(), @@ -296,6 +319,7 @@ async fn cross_sign_root() { let new_root_key = test_utils::test_data().join("snakeoil_2.pem"); let old_key_source = LocalKeySource { path: old_root_key.clone(), + password: None, }; let old_key_id = old_key_source .as_sign() @@ -369,11 +393,15 @@ fn append_signature_root() { // Add key_1 for all roles add_keys_all_roles(vec![key_1.to_str().unwrap()], root_json.to_str().unwrap()); // Add key_2 to root - add_key_root(&vec![key_2.to_str().unwrap()], root_json.to_str().unwrap()); + add_key_root( + &vec![key_2.to_str().unwrap()], + root_json.to_str().unwrap(), + None, + ); // Sign root.json with key_1 - sign_root_json(key_1.to_str().unwrap(), root_json.to_str().unwrap()); + sign_root_json(key_1.to_str().unwrap(), root_json.to_str().unwrap(), None); // Sign root.json with key_2 - sign_root_json(key_2.to_str().unwrap(), root_json.to_str().unwrap()); + sign_root_json(key_2.to_str().unwrap(), root_json.to_str().unwrap(), None); //validate number of signatures assert_eq!(get_sign_len(root_json.to_str().unwrap()), 2); @@ -394,9 +422,9 @@ fn add_multiple_keys_root() { root_json.to_str().unwrap(), ); // Sign root.json with key_1 - sign_root_json(key_1.to_str().unwrap(), root_json.to_str().unwrap()); + sign_root_json(key_1.to_str().unwrap(), root_json.to_str().unwrap(), None); // Sign root.json with key_2 - sign_root_json(key_2.to_str().unwrap(), root_json.to_str().unwrap()); + sign_root_json(key_2.to_str().unwrap(), root_json.to_str().unwrap(), None); //validate number of signatures assert_eq!(get_sign_len(root_json.to_str().unwrap()), 2); @@ -413,7 +441,11 @@ fn below_threshold_failure() { // Add key_1 for all roles add_keys_all_roles(vec![key_1.to_str().unwrap()], root_json.to_str().unwrap()); // Add key_2 to root - add_key_root(&vec![key_2.to_str().unwrap()], root_json.to_str().unwrap()); + add_key_root( + &vec![key_2.to_str().unwrap()], + root_json.to_str().unwrap(), + None, + ); // Sign root.json with key_1 fails, when no `--ignore-threshold` is passed sign_root_json_failure(key_1.to_str().unwrap(), root_json.to_str().unwrap()); } @@ -437,3 +469,73 @@ fn set_version_root() { // validate version number assert_eq!(get_version(root_json.to_str().unwrap()), version); } + +#[test] +// Ensure we can create and sign a root file with a password encrypted key +fn create_root_encrypted_key() { + let out_dir = TempDir::new().unwrap(); + let root_json = out_dir.path().join("root.json"); + let key = test_utils::test_data().join("snakeoil_3.pem"); + + // Password used to decrypt key + let password = "test_password"; + // Create and initialise root.json + initialize_root_json(root_json.to_str().unwrap()); + // Add key for all roles + add_key_root( + &vec![key.to_str().unwrap()], + root_json.to_str().unwrap(), + Some(password), + ); + add_key_timestamp( + key.to_str().unwrap(), + root_json.to_str().unwrap(), + Some(password), + ); + add_key_snapshot( + key.to_str().unwrap(), + root_json.to_str().unwrap(), + Some(password), + ); + add_key_targets( + key.to_str().unwrap(), + root_json.to_str().unwrap(), + Some(password), + ); + + // Sign root.json + sign_root_json( + key.to_str().unwrap(), + root_json.to_str().unwrap(), + Some(password), + ); + assert_eq!(get_sign_len(root_json.to_str().unwrap()), 1); +} + +#[test] +// Add encryped key with an invalid password +fn create_root_encrypted_key_invalid_password() { + let out_dir = TempDir::new().unwrap(); + let root_json = out_dir.path().join("root.json"); + let key = test_utils::test_data().join("snakeoil_3.pem"); + + // Invalid password + let password = "invalid_password"; + // Create and initialise root.json + initialize_root_json(root_json.to_str().unwrap()); + // Add key to root role + let mut cmd = Command::cargo_bin("tuftool").unwrap(); + cmd.args([ + "root", + "add-key", + root_json.to_str().unwrap(), + "--role", + "root", + ]) + .arg("-k") + .arg(key.to_str().unwrap()); + if !password.is_empty() { + cmd.args(["--password", password]); + } + cmd.assert().failure(); +}