diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 33feea1c..af1286e3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -13,6 +13,7 @@ jobs: - run: cargo build --locked -p olpc-cjson - run: cargo build --locked -p tough - run: cargo build --locked -p tough-ssm + - run: cargo build --locked -p tough-kms - run: cargo build --locked -p tuftool - run: cargo test --locked - run: cd tough && cargo test --all-features --locked diff --git a/Cargo.lock b/Cargo.lock index db3b5533..7d34fcb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,11 +178,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "bytes" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "loom 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", -] [[package]] name = "cc" @@ -503,18 +500,6 @@ dependencies = [ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "generator" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "generic-array" version = "0.12.3" @@ -555,7 +540,7 @@ name = "h2" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -620,7 +605,7 @@ name = "http" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -630,7 +615,7 @@ name = "http-body" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -644,7 +629,7 @@ name = "hyper" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -667,7 +652,7 @@ name = "hyper-rustls" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -684,7 +669,7 @@ name = "hyper-tls" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)", "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", @@ -757,16 +742,6 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "loom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "generator 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)", - "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "maplit" version = "1.0.2" @@ -1236,7 +1211,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "encoding_rs 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1284,7 +1259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "async-trait 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1325,13 +1300,40 @@ dependencies = [ "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rusoto_kms" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "async-trait 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_core 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rusoto_mock" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "async-trait 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_core 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rusoto_signature" version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "hmac 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1355,7 +1357,7 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "async-trait 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_core 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1431,11 +1433,6 @@ dependencies = [ "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "scoped-tls" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "scopeguard" version = "1.1.0" @@ -1808,7 +1805,7 @@ name = "tokio" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1861,7 +1858,7 @@ name = "tokio-util" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1893,6 +1890,24 @@ dependencies = [ "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tough-kms" +version = "0.1.0" +dependencies = [ + "bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "pem 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_core 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_credential 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_kms 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_mock 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tough 0.8.0", +] + [[package]] name = "tough-ssm" version = "0.3.0" @@ -1939,6 +1954,7 @@ dependencies = [ "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_core 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_credential 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_kms 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_ssm 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1948,6 +1964,7 @@ dependencies = [ "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "tough 0.8.0", + "tough-kms 0.1.0", "tough-ssm 0.3.0", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2228,7 +2245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" "checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" "checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" -"checksum bytes 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b" +"checksum bytes 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" "checksum cc 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)" = "0fde55d2a2bfaa4c9668bbc63f531fbdeee3ffe188f4662511ce2c22b3eedebe" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" @@ -2268,7 +2285,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" "checksum futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" "checksum futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" -"checksum generator 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68" "checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" "checksum gimli 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" @@ -2295,7 +2311,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" -"checksum loom 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7" "checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" @@ -2354,6 +2369,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)" = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" "checksum rusoto_core 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)" = "841ca8f73e7498ba39146ab43acea906bbbb807d92ec0b7ea4b6293d2621f80d" "checksum rusoto_credential 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60669ddc1bdbb83ce225593649d36b4c5f6bf9db47cc1ab3e81281abffc853f4" +"checksum rusoto_kms 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c5a083f44d08db76d4deedd7527bb215dd008fa08f4b1d8ca40071522bdbcb7" +"checksum rusoto_mock 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6fb65b30ea51d8f975eba8e03e6c10ed247f11a6f1b8fa225c2b844e98113366" "checksum rusoto_signature 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9eddff187ac18c5a91d9ccda9353f30cf531620dce437c4db661dfe2e23b2029" "checksum rusoto_ssm 0.44.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e9224ad97be05dae1a0f6745252f3fa1430d6bea97c93f59e99edaeb7d70f5d" "checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" @@ -2364,7 +2381,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" "checksum same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" "checksum schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" -"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" "checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" "checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" "checksum security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" diff --git a/Cargo.toml b/Cargo.toml index 430118ff..0ed642f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ members = [ "olpc-cjson", "tough", "tough-ssm", + "tough-kms", "tuftool", ] diff --git a/tough-kms/CHANGELOG.md b/tough-kms/CHANGELOG.md new file mode 100644 index 00000000..2e853f1a --- /dev/null +++ b/tough-kms/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2020-07-25 +### Added +- Everything! + +[0.1.0]: https://github.com/awslabs/tough/releases/tag/tough-kms-v0.1.0 diff --git a/tough-kms/Cargo.toml b/tough-kms/Cargo.toml new file mode 100644 index 00000000..0a4038e2 --- /dev/null +++ b/tough-kms/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "tough-kms" +version = "0.1.0" +description = "Implements AWS KMS as a key source for TUF signing keys" +authors = ["Shailesh Gothi "] +license = "MIT OR Apache-2.0" +repository = "https://github.com/awslabs/tough" +keywords = ["TUF", "KMS"] +edition = "2018" + +[features] +default = ["rusoto"] +rusoto = ["rusoto-rustls"] +rusoto-native-tls = ["rusoto_core/native-tls", "rusoto_credential", "rusoto_kms/native-tls"] +rusoto-rustls = ["rusoto_core/rustls", "rusoto_credential", "rusoto_kms/rustls"] + +[dependencies] +tough = { version = "0.8.0", path = "../tough", features = ["http"] } +ring = { version = "0.16.13", features = ["std"] } +rusoto_core = { version = "0.44", optional = true, default-features = false } +rusoto_credential = { version = "0.44", optional = true } +rusoto_kms = { version = "0.44", optional = true, default-features = false } +bytes = "0.5.6" +snafu = { version = "0.6.6", features = ["backtraces-impl-backtrace-crate"] } +tokio = "0.2.13" +pem = "0.8.1" + +[dev-dependencies] +rusoto_mock = { version = "0.44", default-features = false } +serde = "1.0.105" +serde_json = "1.0.50" diff --git a/tough-kms/LICENSE-APACHE b/tough-kms/LICENSE-APACHE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/tough-kms/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + 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/tough-kms/LICENSE-MIT b/tough-kms/LICENSE-MIT new file mode 100644 index 00000000..503351e0 --- /dev/null +++ b/tough-kms/LICENSE-MIT @@ -0,0 +1,8 @@ +MIT License +Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +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/tough-kms/README.md b/tough-kms/README.md new file mode 100644 index 00000000..18c23d1c --- /dev/null +++ b/tough-kms/README.md @@ -0,0 +1,2 @@ +tough-kms implements the `KeySource` trait found in [tough, a Rust TUF client](https://github.com/awslabs/tough). +By implementing this trait, AWS KMS can become a source of keys used to sign a [TUF repository](https://theupdateframework.github.io/). diff --git a/tough-kms/src/client.rs b/tough-kms/src/client.rs new file mode 100644 index 00000000..5e983249 --- /dev/null +++ b/tough-kms/src/client.rs @@ -0,0 +1,33 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::error::{self, Result}; +use rusoto_core::{HttpClient, Region}; +use rusoto_credential::ProfileProvider; +use rusoto_kms::KmsClient; +use snafu::ResultExt; +use std::str::FromStr; + +/// Builds a KMS client for a given profile name. +pub(crate) fn build_client_kms(profile: Option<&str>) -> Result { + Ok(if let Some(profile) = profile { + let mut provider = ProfileProvider::new().context(error::RusotoCreds)?; + provider.set_profile(profile); + let region = provider + .region_from_profile() + .context(error::RusotoRegionFromProfile { profile })?; + + KmsClient::new_with( + HttpClient::new().context(error::RusotoTls)?, + provider, + match region { + Some(region) => { + Region::from_str(®ion).context(error::RusotoRegion { region })? + } + None => Region::default(), + }, + ) + } else { + KmsClient::new(Region::default()) + }) +} diff --git a/tough-kms/src/error.rs b/tough-kms/src/error.rs new file mode 100644 index 00000000..058cd5b6 --- /dev/null +++ b/tough-kms/src/error.rs @@ -0,0 +1,100 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Contains the error type for this library. + +#![allow(clippy::default_trait_access)] + +use snafu::{Backtrace, Snafu}; + +/// Alias for `Result`. +pub type Result = std::result::Result; + +/// The error type for this library. +#[derive(Debug, Snafu)] +#[snafu(visibility = "pub(crate)")] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum Error { + /// The library failed to authenticate Aws account. + #[snafu(display("Error creating AWS credentials provider: {}", source))] + RusotoCreds { + source: rusoto_credential::CredentialsError, + backtrace: Backtrace, + }, + + /// The library failed to get the region for the given profile. + #[snafu(display("Unable to determine region from profile '{}': {}", profile, source))] + RusotoRegionFromProfile { + profile: String, + source: rusoto_credential::CredentialsError, + backtrace: Backtrace, + }, + + /// The library failed to identify the region obtained from the given profile. + #[snafu(display("Unknown AWS region '{}': {}", region, source))] + RusotoRegion { + region: String, + source: rusoto_core::region::ParseRegionError, + backtrace: Backtrace, + }, + + /// The library failed to instantiate 'HttpClient'. + #[snafu(display("Error creating AWS request dispatcher: {}", source))] + RusotoTls { + source: rusoto_core::request::TlsError, + backtrace: Backtrace, + }, + + /// The library failed to instantiate 'tokio Runtime'. + #[snafu(display("Unable to create tokio runtime: {}", source))] + RuntimeCreation { + source: std::io::Error, + backtrace: Backtrace, + }, + + /// The library failed to get public key from AWS KMS + #[snafu(display( + "Failed to get public key for aws-kms://{}/{} : {}", + profile.as_deref().unwrap_or(""), + key_id, + source, + ))] + KmsGetPublicKey { + profile: Option, + key_id: String, + source: rusoto_core::RusotoError, + backtrace: Backtrace, + }, + + /// Empty public key was returned by AWS KMS + #[snafu(display("Public key does not exist"))] + PublicKeyNone, + + /// Public key could not be parsed as an SPKI document + #[snafu(display("Failed to parse public key: {}", source))] + PublicKeyParse { source: tough::schema::Error }, + + /// The library failed to get the message signature from AWS KMS + #[snafu(display("Error while signing message for aws-kms://{}/{} : {}", profile.as_deref().unwrap_or(""), key_id, source))] + KmsSignMessage { + key_id: String, + profile: Option, + source: rusoto_core::RusotoError, + backtrace: Backtrace, + }, + + /// Empty signature was returned by AWS KMS + #[snafu(display("Empty signature returned by AWS KMS"))] + SignatureNotFound, + + /// Provided signing algorithm is not valid + #[snafu(display("Please provide valid signing algorithm"))] + ValidSignAlgorithm, + + /// Supported signing algorithm list is missing for CMK in AWS KMS + #[snafu(display( + "Found public key from AWS KMS, but list of supported signing algorithm is missing" + ))] + MissingSignAlgorithm, +} diff --git a/tough-kms/src/lib.rs b/tough-kms/src/lib.rs new file mode 100644 index 00000000..c8decd46 --- /dev/null +++ b/tough-kms/src/lib.rs @@ -0,0 +1,194 @@ +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! tough-kms implements the `KeySource` trait found in [tough, a Rust TUF client](https://github.com/awslabs/tough). +//! +//! By implementing this trait, AWS KMS can become a source of keys used to sign a [TUF repository](https://theupdateframework.github.io/). +//! +//! # Testing +//! +//! Unit tests are run in the usual manner: `cargo test`. + +#![forbid(missing_debug_implementations, missing_copy_implementations)] +#![deny(rust_2018_idioms)] +// missing_docs is on its own line to make it easy to comment out when making changes. +#![deny(missing_docs)] +#![warn(clippy::pedantic)] +#![allow( + clippy::module_name_repetitions, + clippy::must_use_candidate, + clippy::missing_errors_doc +)] + +mod client; +pub mod error; +use ring::digest::{digest, SHA256}; +use ring::rand::SecureRandom; +use rusoto_kms::{Kms, KmsClient, SignRequest}; +use snafu::{ensure, OptionExt, ResultExt}; +use std::collections::HashMap; +use std::fmt; +use tough::key_source::KeySource; +use tough::schema::decoded::{Decoded, RsaPem}; +use tough::schema::key::{Key, RsaKey, RsaScheme}; +use tough::sign::Sign; + +/// Represents a Signing Algorithms for AWS KMS. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Copy)] +pub enum KmsSigningAlgorithm { + /// Signing Algorithm `RSASSA_PSS_SHA_256` + RsassaPssSha256, +} + +impl KmsSigningAlgorithm { + fn value(self) -> String { + // Currently we are supporting only single algorithm, but code stub is added to support + // multiple algorithms in future. + String::from(match self { + KmsSigningAlgorithm::RsassaPssSha256 => "RSASSA_PSS_SHA_256", + }) + } +} + +/// Implements the `KeySource` trait for keys that live in AWS KMS +pub struct KmsKeySource { + /// Identifies AWS account named profile, if not provided default AWS profile is used. + pub profile: Option, + /// Identifies an asymmetric CMK in AWS KMS. + pub key_id: String, + /// KmsClient Object to query AWS KMS + pub client: Option, + /// Signing Algorithm to be used for the message digest, only `KmsSigningAlgorithm::RsassaPssSha256` is supported at present. + pub signing_algorithm: KmsSigningAlgorithm, +} + +impl fmt::Debug for KmsKeySource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KmsKeySource") + .field("key_id", &self.key_id) + .field("profile", &self.profile) + .finish() + } +} + +/// Implement the `KeySource` trait. +impl KeySource for KmsKeySource { + fn as_sign( + &self, + ) -> std::result::Result, Box> + { + let kms_client = match self.client.to_owned() { + Some(value) => value, + None => client::build_client_kms(self.profile.as_deref())?, + }; + // Get the public key from AWS KMS + let fut = kms_client.get_public_key(rusoto_kms::GetPublicKeyRequest { + key_id: self.key_id.clone(), + ..rusoto_kms::GetPublicKeyRequest::default() + }); + let response = tokio::runtime::Runtime::new() + .context(error::RuntimeCreation)? + .block_on(fut) + .context(error::KmsGetPublicKey { + profile: self.profile.clone(), + key_id: self.key_id.clone(), + })?; + let key = pem::encode_config( + &pem::Pem { + tag: String::from("PUBLIC KEY"), + contents: response.public_key.context(error::PublicKeyNone)?.to_vec(), + }, + pem::EncodeConfig { + line_ending: pem::LineEnding::LF, + }, + ); + ensure!( + response + .signing_algorithms + .context(error::MissingSignAlgorithm)? + .contains(&self.signing_algorithm.value()), + error::ValidSignAlgorithm + ); + Ok(Box::new(KmsRsaKey { + profile: self.profile.clone(), + client: Some(kms_client.clone()), + key_id: self.key_id.clone(), + public_key: key.parse().context(error::PublicKeyParse)?, + signing_algorithm: self.signing_algorithm.value(), + })) + } + + fn write( + &self, + _value: &str, + _key_id_hex: &str, + ) -> Result<(), Box> { + Ok(()) + } +} + +/// Implements the Sign trait for KMS rsa Key +pub struct KmsRsaKey { + /// Key Id of Customer Managed Key in KMS used to sign the message + key_id: String, + /// Aws account profile + profile: Option, + /// KmsClient Object to query AWS KMS + client: Option, + /// Public Key corresponding to Customer Managed Key + public_key: Decoded, + /// Signing Algorithm to be used for the Customer Managed Key + signing_algorithm: String, +} + +impl fmt::Debug for KmsRsaKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KmsRsaKey") + .field("key_id", &self.key_id) + .field("signing_algorithm", &self.signing_algorithm) + .field("public_key", &self.public_key) + .finish() + } +} + +impl Sign for KmsRsaKey { + fn tuf_key(&self) -> Key { + // Create a Key struct for the public key + Key::Rsa { + keyval: RsaKey { + public: self.public_key.to_owned(), + _extra: HashMap::new(), + }, + scheme: RsaScheme::RsassaPssSha256, + _extra: HashMap::new(), + } + } + + fn sign( + &self, + msg: &[u8], + _rng: &dyn SecureRandom, + ) -> Result, Box> { + let kms_client = match self.client.to_owned() { + Some(value) => value, + None => client::build_client_kms(self.profile.as_deref())?, + }; + let sign_fut = kms_client.sign(SignRequest { + key_id: self.key_id.clone(), + message: digest(&SHA256, msg).as_ref().to_vec().into(), + message_type: Some(String::from("DIGEST")), + signing_algorithm: self.signing_algorithm.clone(), + ..rusoto_kms::SignRequest::default() + }); + let response = tokio::runtime::Runtime::new() + .context(error::RuntimeCreation)? + .block_on(sign_fut) + .context(error::KmsSignMessage { + profile: self.profile.clone(), + key_id: self.key_id.clone(), + })?; + let signature = response.signature.context(error::SignatureNotFound)?; + Ok(signature.to_vec()) + } +} diff --git a/tough-kms/tests/all_test.rs b/tough-kms/tests/all_test.rs new file mode 100644 index 00000000..1c239606 --- /dev/null +++ b/tough-kms/tests/all_test.rs @@ -0,0 +1,366 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 +mod test_utils; +use rusoto_kms::KmsClient; +extern crate rusoto_mock; +use self::rusoto_mock::*; +use ring::rand::SystemRandom; +use rusoto_core::signature::SignedRequest; +use rusoto_core::{HttpDispatchError, Region}; +use serde::Deserialize; +use std::fs::File; +use std::io::BufReader; +use tough::key_source::KeySource; +use tough::schema::decoded::{Decoded, RsaPem}; +use tough::schema::key::Key; +use tough_kms::KmsKeySource; +use tough_kms::KmsSigningAlgorithm::RsassaPssSha256; + +#[derive(Default, Debug, Clone, PartialEq, Deserialize)] +struct PublicKeyResp { + #[serde(rename = "PublicKey")] + #[serde( + deserialize_with = "::rusoto_core::serialization::SerdeBlob::deserialize_blob", + serialize_with = "::rusoto_core::serialization::SerdeBlob::serialize_blob", + default + )] + #[serde(skip_serializing_if = "Option::is_none")] + public_key: bytes::Bytes, +} + +#[derive(Deserialize, Debug)] +struct ExpectedPublicKey { + public_key: Decoded, +} + +#[derive(Default, Debug, Clone, PartialEq, Deserialize)] +struct SignResp { + #[serde(rename = "Signature")] + #[serde( + deserialize_with = "::rusoto_core::serialization::SerdeBlob::deserialize_blob", + serialize_with = "::rusoto_core::serialization::SerdeBlob::serialize_blob", + default + )] + #[serde(skip_serializing_if = "Option::is_none")] + signature: bytes::Bytes, +} + +#[derive(Default, Debug, Clone, PartialEq, Deserialize)] +struct CreateKeyResp { + #[serde(rename = "KeyId")] + #[serde(skip_serializing_if = "Option::is_none")] + key_id: String, +} + +#[test] +// Ensure public key is returned on calling tuf_key +fn check_tuf_key_success() { + let input = "response_public_key.json"; + let key_id = String::from("alias/some_alias"); + let file = File::open( + test_utils::test_data() + .join("expected_public_key.json") + .to_str() + .unwrap(), + ) + .unwrap(); + let reader = BufReader::new(file); + let expected_key: Key = serde_json::from_reader(reader).unwrap(); + let mock = MockRequestDispatcher::default() + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.GetPublicKey"))); + }) + .with_body( + MockResponseReader::read_response(test_utils::test_data().to_str().unwrap(), input) + .as_ref(), + ); + let mock_client = KmsClient::new_with(mock, MockCredentialsProvider, Region::UsEast1); + let kms_key = KmsKeySource { + profile: None, + key_id: key_id.clone(), + client: Some(mock_client), + signing_algorithm: RsassaPssSha256, + }; + let sign = kms_key.as_sign().unwrap(); + let key = sign.tuf_key(); + assert!(matches!(key, Key::Rsa { .. })); + assert_eq!(key, expected_key); +} + +#[test] +// Ensure message signature is returned on calling sign +fn check_sign_success() { + let resp_public_key = "response_public_key.json"; + let resp_signature = "response_signature.json"; + let file = File::open( + test_utils::test_data() + .join(resp_signature) + .to_str() + .unwrap(), + ) + .unwrap(); + let reader = BufReader::new(file); + let expected_json: SignResp = serde_json::from_reader(reader).unwrap(); + let expected_signature = expected_json.signature.to_vec(); + let mock = MultipleMockRequestDispatcher::new(vec![ + MockRequestDispatcher::with_status(200) + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.GetPublicKey"))); + }) + .with_body( + MockResponseReader::read_response( + test_utils::test_data().to_str().unwrap(), + resp_public_key, + ) + .as_ref(), + ), + MockRequestDispatcher::with_status(200) + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.Sign"))); + }) + .with_body( + MockResponseReader::read_response( + test_utils::test_data().to_str().unwrap(), + resp_signature, + ) + .as_ref(), + ), + ]); + let mock_client = KmsClient::new_with(mock, MockCredentialsProvider, Region::UsEast1); + let kms_key = KmsKeySource { + profile: None, + key_id: String::from("alias/some_alias"), + client: Some(mock_client), + signing_algorithm: RsassaPssSha256, + }; + let rng = SystemRandom::new(); + let kms_sign = kms_key.as_sign().unwrap(); + let signature = kms_sign + .sign("Some message to sign".as_bytes(), &rng) + .unwrap(); + assert_eq!(signature, expected_signature); +} + +#[test] +// Ensure call to tuf_key fails when public key is not available +fn check_public_key_failure() { + let error_msg = String::from("Some error message from KMS"); + let mock = + MockRequestDispatcher::with_dispatch_error(HttpDispatchError::new(error_msg.clone())); + let client = KmsClient::new_with(mock, MockCredentialsProvider, Region::UsEast1); + let key_id = String::from("alias/some_alias"); + let kms_key = KmsKeySource { + profile: None, + key_id: key_id.clone(), + client: Some(client), + signing_algorithm: RsassaPssSha256, + }; + let result = kms_key.as_sign(); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!( + format!( + "Failed to get public key for aws-kms:///{} : {}", + key_id.clone(), + error_msg.clone() + ), + err.to_string() + ); +} + +#[test] +// Ensure call to as_sign fails when signing algorithms are missing in get_public_key response +fn check_public_key_missing_algo() { + let input = "response_public_key_no_algo.json"; + let key_id = String::from("alias/some_alias"); + let mock = MockRequestDispatcher::default() + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.GetPublicKey"))); + }) + .with_body( + MockResponseReader::read_response(test_utils::test_data().to_str().unwrap(), input) + .as_ref(), + ); + let mock_client = KmsClient::new_with(mock, MockCredentialsProvider, Region::UsEast1); + let kms_key = KmsKeySource { + profile: None, + key_id: key_id.clone(), + client: Some(mock_client), + signing_algorithm: RsassaPssSha256, + }; + let err = kms_key.as_sign().err().unwrap(); + assert_eq!( + String::from( + "Found public key from AWS KMS, but list of supported signing algorithm is missing" + ), + err.to_string() + ); +} + +#[test] +// Ensure call to as_sign fails when provided signing algorithm does not match +fn check_public_key_unmatch_algo() { + let input = "response_public_key_unmatch_algo.json"; + let key_id = String::from("alias/some_alias"); + let mock = MockRequestDispatcher::default() + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.GetPublicKey"))); + }) + .with_body( + MockResponseReader::read_response(test_utils::test_data().to_str().unwrap(), input) + .as_ref(), + ); + let mock_client = KmsClient::new_with(mock, MockCredentialsProvider, Region::UsEast1); + let kms_key = KmsKeySource { + profile: None, + key_id: key_id.clone(), + client: Some(mock_client), + signing_algorithm: RsassaPssSha256, + }; + let err = kms_key.as_sign().err().unwrap(); + assert_eq!( + String::from("Please provide valid signing algorithm"), + err.to_string() + ); +} + +#[test] +// Ensure sign error when Kms fails to sign message. +fn check_sign_request_failure() { + let error_msg = String::from("Some error message from KMS"); + let resp_public_key = "response_public_key.json"; + let key_id = String::from("alias/some_alias"); + let mock = MultipleMockRequestDispatcher::new(vec![ + MockRequestDispatcher::with_status(200) + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.GetPublicKey"))); + }) + .with_body( + MockResponseReader::read_response( + test_utils::test_data().to_str().unwrap(), + resp_public_key, + ) + .as_ref(), + ), + MockRequestDispatcher::with_dispatch_error(HttpDispatchError::new(error_msg.clone())) + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.Sign"))); + }), + ]); + let mock_client = KmsClient::new_with(mock, MockCredentialsProvider, Region::UsEast1); + let kms_key = KmsKeySource { + profile: None, + key_id: key_id.clone(), + client: Some(mock_client), + signing_algorithm: RsassaPssSha256, + }; + let rng = SystemRandom::new(); + let kms_sign = kms_key.as_sign().unwrap(); + let result = kms_sign.sign("Some message to sign".as_bytes(), &rng); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!( + format!( + "Error while signing message for aws-kms:///{} : {}", + key_id.clone(), + error_msg.clone() + ), + err.to_string() + ); +} + +#[test] +// Ensure sign error when Kms returns empty signature. +fn check_signature_failure() { + let resp_public_key = "response_public_key.json"; + let resp_signature = "response_signature_empty.json"; + let key_id = String::from("alias/some_alias"); + let mock = MultipleMockRequestDispatcher::new(vec![ + MockRequestDispatcher::with_status(200) + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.GetPublicKey"))); + }) + .with_body( + MockResponseReader::read_response( + test_utils::test_data().to_str().unwrap(), + resp_public_key, + ) + .as_ref(), + ), + MockRequestDispatcher::with_status(200) + .with_request_checker(|request: &SignedRequest| { + assert!(request + .headers + .get("x-amz-target") + .unwrap() + .contains(&Vec::from("TrentService.Sign"))); + }) + .with_body( + MockResponseReader::read_response( + test_utils::test_data().to_str().unwrap(), + resp_signature, + ) + .as_ref(), + ), + ]); + let mock_client = KmsClient::new_with(mock, MockCredentialsProvider, Region::UsEast1); + let kms_key = KmsKeySource { + profile: None, + key_id: key_id.clone(), + client: Some(mock_client), + signing_algorithm: RsassaPssSha256, + }; + let rng = SystemRandom::new(); + let kms_sign = kms_key.as_sign().unwrap(); + let result = kms_sign.sign("Some message to sign".as_bytes(), &rng); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!( + format!("Empty signature returned by AWS KMS"), + err.to_string() + ); +} + +#[test] +fn check_write_ok() { + let key_id = String::from("alias/some_alias"); + let kms_key = KmsKeySource { + profile: None, + key_id: key_id.clone(), + client: None, + signing_algorithm: RsassaPssSha256, + }; + assert_eq!((), kms_key.write("", "").unwrap()) +} diff --git a/tough-kms/tests/data/expected_public_key.json b/tough-kms/tests/data/expected_public_key.json new file mode 100644 index 00000000..dbbc72a1 --- /dev/null +++ b/tough-kms/tests/data/expected_public_key.json @@ -0,0 +1,7 @@ +{ + "keytype": "rsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwFWHzkki38xzt2d/jECK\nNVdHe/y1o6dPcdwUz07bnbtVKyT1Cw5iemChnIGwUNpsqmrOov2+wetWEW/zrCeH\npQtqBZFeSo689N99WVFSA48xWEYLHutYY0eOSQ+RZzrLXaAPnXlu93F8icLNHBU3\n7fSd8t5mpoj6tIn9M50mRp1qk1qJpgajB6a2W+T1yMur2kQkqzJLZQg4kxqXDCdg\ngxb8DWdRnT/m2K9b/RziwqGJbNPnxGe3A0n8HCNOF1IuTJeKn8vbQMoP3cXE6+TI\nNmZzU28IdiNDvN5+zlHFoxsNRL/yHJPX/8PVGNu+Gg2xBfmnYYzSR9hm9O1GBLix\nkwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "scheme": "rsassa-pss-sha256" +} diff --git a/tough-kms/tests/data/response_create_key.json b/tough-kms/tests/data/response_create_key.json new file mode 100644 index 00000000..4315e114 --- /dev/null +++ b/tough-kms/tests/data/response_create_key.json @@ -0,0 +1,5 @@ +{ + "KeyMetadata" : { + "KeyId": "SomeKeyId" + } +} diff --git a/tough-kms/tests/data/response_public_key.json b/tough-kms/tests/data/response_public_key.json new file mode 100644 index 00000000..fd252250 --- /dev/null +++ b/tough-kms/tests/data/response_public_key.json @@ -0,0 +1,14 @@ +{ + "KeyId": "arn:aws:kms:us-west-2:062205370538:key/3bbf2655-2dff-4040-ad37-2ec8f60d651b", + "PublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwFWHzkki38xzt2d/jECKNVdHe/y1o6dPcdwUz07bnbtVKyT1Cw5iemChnIGwUNpsqmrOov2+wetWEW/zrCeHpQtqBZFeSo689N99WVFSA48xWEYLHutYY0eOSQ+RZzrLXaAPnXlu93F8icLNHBU37fSd8t5mpoj6tIn9M50mRp1qk1qJpgajB6a2W+T1yMur2kQkqzJLZQg4kxqXDCdggxb8DWdRnT/m2K9b/RziwqGJbNPnxGe3A0n8HCNOF1IuTJeKn8vbQMoP3cXE6+TINmZzU28IdiNDvN5+zlHFoxsNRL/yHJPX/8PVGNu+Gg2xBfmnYYzSR9hm9O1GBLixkwIDAQAB", + "CustomerMasterKeySpec": "RSA_2048", + "KeyUsage": "SIGN_VERIFY", + "SigningAlgorithms": [ + "RSASSA_PKCS1_V1_5_SHA_256", + "RSASSA_PKCS1_V1_5_SHA_384", + "RSASSA_PKCS1_V1_5_SHA_512", + "RSASSA_PSS_SHA_256", + "RSASSA_PSS_SHA_384", + "RSASSA_PSS_SHA_512" + ] +} diff --git a/tough-kms/tests/data/response_public_key_no_algo.json b/tough-kms/tests/data/response_public_key_no_algo.json new file mode 100644 index 00000000..43121a0a --- /dev/null +++ b/tough-kms/tests/data/response_public_key_no_algo.json @@ -0,0 +1,6 @@ +{ + "KeyId": "arn:aws:kms:us-west-2:062205370538:key/3bbf2655-2dff-4040-ad37-2ec8f60d651b", + "PublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwFWHzkki38xzt2d/jECKNVdHe/y1o6dPcdwUz07bnbtVKyT1Cw5iemChnIGwUNpsqmrOov2+wetWEW/zrCeHpQtqBZFeSo689N99WVFSA48xWEYLHutYY0eOSQ+RZzrLXaAPnXlu93F8icLNHBU37fSd8t5mpoj6tIn9M50mRp1qk1qJpgajB6a2W+T1yMur2kQkqzJLZQg4kxqXDCdggxb8DWdRnT/m2K9b/RziwqGJbNPnxGe3A0n8HCNOF1IuTJeKn8vbQMoP3cXE6+TINmZzU28IdiNDvN5+zlHFoxsNRL/yHJPX/8PVGNu+Gg2xBfmnYYzSR9hm9O1GBLixkwIDAQAB", + "CustomerMasterKeySpec": "RSA_2048", + "KeyUsage": "SIGN_VERIFY" +} diff --git a/tough-kms/tests/data/response_public_key_unmatch_algo.json b/tough-kms/tests/data/response_public_key_unmatch_algo.json new file mode 100644 index 00000000..f79d3ba2 --- /dev/null +++ b/tough-kms/tests/data/response_public_key_unmatch_algo.json @@ -0,0 +1,13 @@ +{ + "KeyId": "arn:aws:kms:us-west-2:062205370538:key/3bbf2655-2dff-4040-ad37-2ec8f60d651b", + "PublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwFWHzkki38xzt2d/jECKNVdHe/y1o6dPcdwUz07bnbtVKyT1Cw5iemChnIGwUNpsqmrOov2+wetWEW/zrCeHpQtqBZFeSo689N99WVFSA48xWEYLHutYY0eOSQ+RZzrLXaAPnXlu93F8icLNHBU37fSd8t5mpoj6tIn9M50mRp1qk1qJpgajB6a2W+T1yMur2kQkqzJLZQg4kxqXDCdggxb8DWdRnT/m2K9b/RziwqGJbNPnxGe3A0n8HCNOF1IuTJeKn8vbQMoP3cXE6+TINmZzU28IdiNDvN5+zlHFoxsNRL/yHJPX/8PVGNu+Gg2xBfmnYYzSR9hm9O1GBLixkwIDAQAB", + "CustomerMasterKeySpec": "RSA_2048", + "KeyUsage": "SIGN_VERIFY", + "SigningAlgorithms": [ + "RSASSA_PKCS1_V1_5_SHA_256", + "RSASSA_PKCS1_V1_5_SHA_384", + "RSASSA_PKCS1_V1_5_SHA_512", + "RSASSA_PSS_SHA_384", + "RSASSA_PSS_SHA_512" + ] +} diff --git a/tough-kms/tests/data/response_signature.json b/tough-kms/tests/data/response_signature.json new file mode 100644 index 00000000..ba0d01c2 --- /dev/null +++ b/tough-kms/tests/data/response_signature.json @@ -0,0 +1,3 @@ +{ + "Signature" : "U29tZSBtZXNzYWdlIHRvIHNpZ24=" +} diff --git a/tough-kms/tests/data/response_signature_empty.json b/tough-kms/tests/data/response_signature_empty.json new file mode 100644 index 00000000..2c63c085 --- /dev/null +++ b/tough-kms/tests/data/response_signature_empty.json @@ -0,0 +1,2 @@ +{ +} diff --git a/tough-kms/tests/test_utils.rs b/tough-kms/tests/test_utils.rs new file mode 100644 index 00000000..f4c6d47c --- /dev/null +++ b/tough-kms/tests/test_utils.rs @@ -0,0 +1,11 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::path::PathBuf; + +/// Returns the path to our test data directory +pub fn test_data() -> PathBuf { + let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + p.pop(); + p.join("tough-kms").join("tests").join("data") +} diff --git a/tough/src/editor/signed.rs b/tough/src/editor/signed.rs index c8f41272..28e5a781 100644 --- a/tough/src/editor/signed.rs +++ b/tough/src/editor/signed.rs @@ -77,7 +77,7 @@ where role: T::TYPE.to_string(), })?; for (signing_key_id, signing_key) in valid_keys { - let sig = signing_key.sign(&data, rng)?; + let sig = signing_key.sign(&data, rng).context(error::SignMessage)?; // Add the signatures to the `Signed` struct for this role role.signatures.push(Signature { diff --git a/tough/src/error.rs b/tough/src/error.rs index d657b319..549de08a 100644 --- a/tough/src/error.rs +++ b/tough/src/error.rs @@ -287,6 +287,12 @@ pub enum Error { backtrace: Backtrace, }, + #[snafu(display("Failed to sign message: {}", source))] + SignMessage { + source: Box, + backtrace: Backtrace, + }, + #[snafu(display("Unable to find signing keys for role '{}'", role))] SigningKeysNotFound { role: String }, diff --git a/tough/src/sign.rs b/tough/src/sign.rs index a6d2e3da..27d73967 100644 --- a/tough/src/sign.rs +++ b/tough/src/sign.rs @@ -20,7 +20,11 @@ pub trait Sign: Sync + Send { fn tuf_key(&self) -> crate::schema::key::Key; /// Signs the supplied message - fn sign(&self, msg: &[u8], rng: &dyn SecureRandom) -> Result>; + fn sign( + &self, + msg: &[u8], + rng: &dyn SecureRandom, + ) -> std::result::Result, Box>; } /// Implements the Sign trait for ED25519 @@ -38,7 +42,11 @@ impl Sign for Ed25519KeyPair { } } - fn sign(&self, msg: &[u8], _rng: &dyn SecureRandom) -> Result> { + fn sign( + &self, + msg: &[u8], + _rng: &dyn SecureRandom, + ) -> std::result::Result, Box> { let signature = self.sign(msg); Ok(signature.as_ref().to_vec()) } @@ -59,7 +67,11 @@ impl Sign for RsaKeyPair { } } - fn sign(&self, msg: &[u8], rng: &dyn SecureRandom) -> Result> { + fn sign( + &self, + msg: &[u8], + rng: &dyn SecureRandom, + ) -> std::result::Result, Box> { let mut signature = vec![0; self.public_modulus_len()]; self.sign(&ring::signature::RSA_PSS_SHA256, rng, msg, &mut signature) .context(error::Sign)?; @@ -82,7 +94,11 @@ impl Sign for EcdsaKeyPair { } } - fn sign(&self, msg: &[u8], rng: &dyn SecureRandom) -> Result> { + fn sign( + &self, + msg: &[u8], + rng: &dyn SecureRandom, + ) -> std::result::Result, Box> { let signature = self.sign(rng, msg).context(error::Sign)?; Ok(signature.as_ref().to_vec()) } @@ -108,7 +124,11 @@ impl Sign for SignKeyPair { } } - fn sign(&self, msg: &[u8], rng: &dyn SecureRandom) -> Result> { + fn sign( + &self, + msg: &[u8], + rng: &dyn SecureRandom, + ) -> std::result::Result, Box> { match self { RSA(key) => (key as &dyn Sign).sign(msg, rng), ED25519(key) => (key as &dyn Sign).sign(msg, rng), diff --git a/tuftool/Cargo.toml b/tuftool/Cargo.toml index 95c78faa..6b6e3cf5 100644 --- a/tuftool/Cargo.toml +++ b/tuftool/Cargo.toml @@ -9,10 +9,11 @@ keywords = ["tuf", "update", "repository"] edition = "2018" [features] +integ = [] default = ["rusoto"] rusoto = ["rusoto-rustls"] -rusoto-native-tls = ["rusoto_core/native-tls", "rusoto_credential", "rusoto_ssm/native-tls"] -rusoto-rustls = ["rusoto_core/rustls", "rusoto_credential", "rusoto_ssm/rustls"] +rusoto-native-tls = ["rusoto_core/native-tls", "rusoto_credential", "rusoto_ssm/native-tls", "rusoto_kms/native-tls"] +rusoto-rustls = ["rusoto_core/rustls", "rusoto_credential", "rusoto_ssm/rustls", "rusoto_kms/rustls",] [dependencies] chrono = "0.4.11" @@ -27,6 +28,7 @@ ring = { version = "0.16.13", features = ["std"] } rusoto_core = { version = "0.44", optional = true, default-features = false } rusoto_credential = { version = "0.44", optional = true } rusoto_ssm = { version = "0.44", optional = true, default-features = false } +rusoto_kms = { version = "0.44", optional = true, default-features = false } serde = "1.0.110" serde_json = "1.0.53" simplelog = "0.8.0" @@ -36,6 +38,7 @@ tempfile = "3.1.0" tokio = "0.2.21" tough = { version = "0.8.0", path = "../tough", features = ["http"] } tough-ssm = { version = "0.3.0", path = "../tough-ssm" } +tough-kms = { version = "0.1.0", path = "../tough-kms" } url = "2.1.0" walkdir = "2.2.9" diff --git a/tuftool/README.md b/tuftool/README.md index 090aceab..cd346b36 100644 --- a/tuftool/README.md +++ b/tuftool/README.md @@ -90,4 +90,10 @@ tuftool create \ ls "${WRK}/tuf-repo/metadata" # and you can see our signed repository's targets here: ls "${WRK}/tuf-repo/targets" -``` \ No newline at end of file +``` + +## Testing + +Unit tests are run in the usual manner: `cargo test`. +Integration tests require working AWS credentials and are disabled by default behind a feature named `integ`. +To run all tests, including integration tests: `cargo test --features 'integ'` or `AWS_PROFILE=test-profile cargo test --features 'integ'` with specific profile. \ No newline at end of file diff --git a/tuftool/src/source.rs b/tuftool/src/source.rs index 9a33e7b6..b7986625 100644 --- a/tuftool/src/source.rs +++ b/tuftool/src/source.rs @@ -37,6 +37,7 @@ use crate::error::{self, Result}; use snafu::ResultExt; use std::path::PathBuf; use tough::key_source::{KeySource, LocalKeySource}; +use tough_kms::{KmsKeySource, KmsSigningAlgorithm}; use tough_ssm::SsmKeySource; use url::Url; @@ -55,7 +56,6 @@ pub(crate) fn parse_key_source(input: &str) -> Result> { .base_url(Some(&pwd_url)) .parse(input) .context(error::UrlParse { url: input })?; - match url.scheme() { "file" => Ok(Box::new(LocalKeySource { path: PathBuf::from(url.path()), @@ -80,6 +80,19 @@ pub(crate) fn parse_key_source(input: &str) -> Result> { } }), })), + "aws-kms" => Ok(Box::new(KmsKeySource { + profile: url.host_str().and_then(|s| { + if s.is_empty() { + None + } else { + Some(s.to_owned()) + } + }), + // remove first '/' from the path to get the key_id + key_id: url.path()[1..].to_string(), + client: None, + signing_algorithm: KmsSigningAlgorithm::RsassaPssSha256, + })), _ => error::UnrecognizedScheme { scheme: url.scheme(), } diff --git a/tuftool/tests/create_repository_integration.rs b/tuftool/tests/create_repository_integration.rs new file mode 100644 index 00000000..e407ec73 --- /dev/null +++ b/tuftool/tests/create_repository_integration.rs @@ -0,0 +1,243 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT OR Apache-2.0 + +mod test_utils; +use assert_cmd::Command; +use chrono::{Duration, Utc}; +use std::env; +use std::fs::File; +use tempfile::TempDir; +use tough::{ExpirationEnforcement, Limits, Repository, Settings}; + +// This file include integration tests for KeySources: tough-ssm, tough-kms and local file key. +// Since the tests are run using the actual "AWS SSM and AWS KMS", you would have to configure +// AWS credentials with root permission. +// Refer https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html to configure named profile. +// Additionally, tough-kms key generation is not supported (issue #211), so you would have to manually create kms CMK key. +// To run test include feature flag 'integ' like : "cargo test --features=integ" + +fn get_profile() -> String { + env::var("AWS_PROFILE").unwrap_or_default() +} + +fn initialize_root_json(root_json: &str) { + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "init", root_json]) + .assert() + .success(); + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "expire", root_json, "3030-09-22T00:00:00Z"]) + .assert() + .success(); + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "set-threshold", root_json, "root", "1"]) + .assert() + .success(); + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "set-threshold", root_json, "snapshot", "1"]) + .assert() + .success(); + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "set-threshold", root_json, "targets", "1"]) + .assert() + .success(); + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "set-threshold", root_json, "timestamp", "1"]) + .assert() + .success(); +} + +fn gen_key(key: &str, root_json: &str) { + Command::cargo_bin("tuftool") + .unwrap() + .args(&[ + "root", + "gen-rsa-key", + root_json, + key, + "--role", + "root", + "-b", + "3072", + ]) + .assert() + .success(); +} +fn add_root_key(key: &str, root_json: &str) { + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "add-key", root_json, key.clone(), "--role", "root"]) + .assert() + .success(); +} +fn add_key_all_role(key: &str, root_json: &str) { + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "add-key", root_json, key, "--role", "snapshot"]) + .assert() + .success(); + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "add-key", root_json, key, "--role", "targets"]) + .assert() + .success(); + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "add-key", root_json, key, "--role", "timestamp"]) + .assert() + .success(); +} + +fn sign_root_json(key: &str, root_json: &str) { + Command::cargo_bin("tuftool") + .unwrap() + .args(&["root", "sign", root_json, key]) + .assert() + .success(); +} + +fn create_repository(root_key: &str, auto_generate: bool) { + // create a root.json file to create TUF repository metadata + let root_json_dir = TempDir::new().unwrap(); + let root_json = root_json_dir.path().join("root.json"); + initialize_root_json(root_json.to_str().unwrap()); + if auto_generate == true { + gen_key(root_key.clone(), root_json.to_str().unwrap()); + } else { + add_root_key(root_key, root_json.to_str().unwrap()); + } + add_key_all_role(root_key.clone(), root_json.to_str().unwrap()); + sign_root_json(root_key.clone(), root_json.to_str().unwrap()); + // Use root.json file to generate metadata using create command. + let timestamp_expiration = Utc::now().checked_add_signed(Duration::days(3)).unwrap(); + let timestamp_version: u64 = 1234; + let snapshot_expiration = Utc::now().checked_add_signed(Duration::days(21)).unwrap(); + let snapshot_version: u64 = 5432; + let targets_expiration = Utc::now().checked_add_signed(Duration::days(13)).unwrap(); + let targets_version: u64 = 789; + let targets_input_dir = test_utils::test_data() + .join("tuf-reference-impl") + .join("targets"); + let repo_dir = TempDir::new().unwrap(); + let load_dir = TempDir::new().unwrap(); + // Create a repo using tuftool and the reference tuf implementation targets + Command::cargo_bin("tuftool") + .unwrap() + .args(&[ + "create", + "-t", + targets_input_dir.to_str().unwrap(), + "-o", + repo_dir.path().to_str().unwrap(), + "-k", + root_key, + "--root", + root_json.to_str().unwrap(), + "--targets-expires", + targets_expiration.to_rfc3339().as_str(), + "--targets-version", + format!("{}", targets_version).as_str(), + "--snapshot-expires", + snapshot_expiration.to_rfc3339().as_str(), + "--snapshot-version", + format!("{}", snapshot_version).as_str(), + "--timestamp-expires", + timestamp_expiration.to_rfc3339().as_str(), + "--timestamp-version", + format!("{}", timestamp_version).as_str(), + ]) + .assert() + .success(); + + // Load our newly created repo + let metadata_base_url = &test_utils::dir_url(repo_dir.path().join("metadata")); + let targets_base_url = &test_utils::dir_url(repo_dir.path().join("targets")); + let repo = Repository::load( + &tough::FilesystemTransport, + Settings { + root: File::open(root_json).unwrap(), + datastore: load_dir.as_ref(), + metadata_base_url, + targets_base_url, + limits: Limits::default(), + expiration_enforcement: ExpirationEnforcement::Safe, + }, + ) + .unwrap(); + + // Ensure we can read the targets + assert_eq!( + test_utils::read_to_end(repo.read_target("file1.txt").unwrap().unwrap()), + &b"This is an example target file."[..] + ); + assert_eq!( + test_utils::read_to_end(repo.read_target("file2.txt").unwrap().unwrap()), + &b"This is an another example target file."[..] + ); + assert_eq!( + test_utils::read_to_end(repo.read_target("file3.txt").unwrap().unwrap()), + &b"This is role1's target file."[..] + ); + + // Ensure the targets.json file is correct + assert_eq!(repo.targets().signed.version.get(), targets_version); + assert_eq!(repo.targets().signed.expires, targets_expiration); + assert_eq!(repo.targets().signed.targets.len(), 3); + assert_eq!(repo.targets().signed.targets["file1.txt"].length, 31); + assert_eq!(repo.targets().signed.targets["file2.txt"].length, 39); + assert_eq!(repo.targets().signed.targets["file3.txt"].length, 28); + assert_eq!(repo.targets().signatures.len(), 1); + + // Ensure the snapshot.json file is correct + assert_eq!(repo.snapshot().signed.version.get(), snapshot_version); + assert_eq!(repo.snapshot().signed.expires, snapshot_expiration); + assert_eq!(repo.snapshot().signed.meta.len(), 1); + assert_eq!( + repo.snapshot().signed.meta["targets.json"].version.get(), + targets_version + ); + assert_eq!(repo.snapshot().signatures.len(), 1); + + // Ensure the timestamp.json file is correct + assert_eq!(repo.timestamp().signed.version.get(), timestamp_version); + assert_eq!(repo.timestamp().signed.expires, timestamp_expiration); + assert_eq!(repo.timestamp().signed.meta.len(), 1); + assert_eq!( + repo.timestamp().signed.meta["snapshot.json"].version.get(), + snapshot_version + ); + assert_eq!(repo.snapshot().signatures.len(), 1); + root_json_dir.close().unwrap(); +} + +#[test] +#[cfg_attr(not(feature = "integ"), ignore)] +// Ensure we can use local rsa key to create and sign a repo created by the `tuftool` binary using the `tough` library +fn create_repository_local_key() { + let root_key_dir = TempDir::new().unwrap(); + let root_key_path = root_key_dir.path().join("local_key.pem"); + let root_key = &format!("file://{}", root_key_path.to_str().unwrap()); + create_repository(root_key, true); +} + +#[test] +#[cfg_attr(not(feature = "integ"), ignore)] +// Ensure we can use ssm key to create and sign a repo created by the `tuftool` binary using the `tough` library +fn create_repository_ssm_key() { + let root_key = &format!("aws-ssm://{}/tough-integ/key-a", get_profile()); + create_repository(root_key, true); +} + +#[test] +#[cfg_attr(not(feature = "integ"), ignore)] +// Ensure we can use kms key to create and sign a repo created by the `tuftool` binary using the `tough` library +fn create_repository_kms_key() { + let root_key = &format!("aws-kms://{}/alias/tough-integ/key-a", get_profile()); + create_repository(root_key, false); +}