diff --git a/.changelog/1764870201.md b/.changelog/1764870201.md new file mode 100644 index 00000000000..7dd8903ef45 --- /dev/null +++ b/.changelog/1764870201.md @@ -0,0 +1,12 @@ +--- +applies_to: +- aws-sdk-rust +authors: +- aajtodd +references: +- aws-sdk-rust#255 +breaking: false +new_feature: true +bug_fix: false +--- +New utility crate `aws-sdk-cloudfront-signer` for generating [signed URLs and Cookies for accessing private distribution content](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html). diff --git a/aws/rust-runtime/Cargo.lock b/aws/rust-runtime/Cargo.lock index e54e8253aa6..d212baaa90c 100644 --- a/aws/rust-runtime/Cargo.lock +++ b/aws/rust-runtime/Cargo.lock @@ -152,6 +152,24 @@ dependencies = [ name = "aws-runtime-api" version = "1.1.10" +[[package]] +name = "aws-sdk-cloudfront-url-signer" +version = "0.1.0" +dependencies = [ + "aws-smithy-async", + "aws-smithy-json", + "aws-smithy-types", + "base64-simd", + "http 1.4.0", + "p256 0.13.2", + "rsa", + "serde", + "serde_json", + "sha1", + "tokio", + "url", +] + [[package]] name = "aws-sigv4" version = "1.3.7" @@ -171,7 +189,7 @@ dependencies = [ "http 0.2.12", "http 1.4.0", "httparse", - "p256", + "p256 0.11.1", "percent-encoding", "pretty_assertions", "proptest", @@ -261,6 +279,13 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-smithy-json" +version = "0.61.8" +dependencies = [ + "aws-smithy-types", +] + [[package]] name = "aws-smithy-observability" version = "0.1.5" @@ -370,6 +395,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64-simd" version = "0.8.0" @@ -612,7 +643,7 @@ checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" dependencies = [ "crc", "digest", - "rand", + "rand 0.9.2", "regex", "rustversion", ] @@ -711,8 +742,10 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ + "generic-array", "rand_core 0.6.4", "subtle", + "zeroize", ] [[package]] @@ -741,6 +774,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -763,6 +807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -784,10 +829,24 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -802,16 +861,36 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", + "base16ct 0.1.1", "crypto-bigint 0.4.9", - "der", + "der 0.6.1", "digest", - "ff", + "ff 0.12.1", "generic-array", - "group", - "pkcs8", + "group 0.12.1", + "pkcs8 0.9.0", "rand_core 0.6.4", - "sec1", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest", + "ff 0.13.1", + "generic-array", + "group 0.13.0", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -848,6 +927,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" @@ -922,6 +1011,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -953,7 +1043,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] @@ -1311,6 +1412,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1318,6 +1422,12 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1425,6 +1535,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1440,6 +1566,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -1458,6 +1595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1490,8 +1628,20 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder", "sha2", ] @@ -1518,6 +1668,15 @@ dependencies = [ "windows-link", ] +[[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.2" @@ -1536,14 +1695,35 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + [[package]] name = "pkcs8" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", ] [[package]] @@ -1608,6 +1788,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1627,8 +1816,8 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand", - "rand_chacha", + "rand 0.9.2", + "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -1657,16 +1846,36 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -1779,6 +1988,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -1802,6 +2021,26 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1931,10 +2170,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -2073,6 +2326,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.11" @@ -2105,6 +2368,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.6.0" @@ -2112,7 +2381,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", ] [[package]] diff --git a/aws/rust-runtime/Cargo.toml b/aws/rust-runtime/Cargo.toml index 34ec57c49d3..ec9ea1a2db6 100644 --- a/aws/rust-runtime/Cargo.toml +++ b/aws/rust-runtime/Cargo.toml @@ -9,6 +9,7 @@ members = [ "aws-runtime-api", "aws-sigv4", "aws-types", + "aws-sdk-cloudfront-signer" ] exclude = ["aws-config"] diff --git a/aws/rust-runtime/aws-config/Cargo.lock b/aws/rust-runtime/aws-config/Cargo.lock index c4f02efd9f0..f3d8bb098d7 100644 --- a/aws/rust-runtime/aws-config/Cargo.lock +++ b/aws/rust-runtime/aws-config/Cargo.lock @@ -56,7 +56,7 @@ dependencies = [ "fastrand", "futures-util", "hex", - "http 1.4.0", + "http 1.3.1", "p256", "rand", "ring", @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.1" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" dependencies = [ "aws-lc-sys", "zeroize", @@ -95,10 +95,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.34.0" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" dependencies = [ + "bindgen", "cc", "cmake", "dunce", @@ -222,7 +223,7 @@ dependencies = [ "hex", "hmac", "http 0.2.12", - "http 1.4.0", + "http 1.3.1", "percent-encoding", "sha2", "time", @@ -249,7 +250,7 @@ dependencies = [ "futures-core", "futures-util", "http 0.2.12", - "http 1.4.0", + "http 1.3.1", "http-body 0.4.6", "percent-encoding", "pin-project-lite", @@ -269,11 +270,11 @@ dependencies = [ "h2 0.3.27", "h2 0.4.12", "http 0.2.12", - "http 1.4.0", + "http 1.3.1", "http-body 0.4.6", "http-body 1.0.1", "hyper 0.14.32", - "hyper 1.8.1", + "hyper 1.8.0", "hyper-rustls", "hyper-util", "indexmap", @@ -341,7 +342,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.0", + "http 1.3.1", "http-body 0.4.6", "http-body 1.0.1", "pin-project-lite", @@ -359,7 +360,7 @@ dependencies = [ "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.0", + "http 1.3.1", "pin-project-lite", "tokio", "tracing", @@ -374,7 +375,7 @@ dependencies = [ "bytes", "bytes-utils", "http 0.2.12", - "http 1.4.0", + "http 1.3.1", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -434,6 +435,26 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -466,9 +487,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.11.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytes-utils" @@ -501,9 +522,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.48" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "jobserver", @@ -511,6 +532,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -553,6 +583,17 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "cmake" version = "0.1.54" @@ -746,9 +787,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "fnv" @@ -844,6 +885,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "group" version = "0.13.0" @@ -885,7 +932,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -906,9 +953,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hex" @@ -938,11 +985,12 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", + "fnv", "itoa", ] @@ -964,7 +1012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.3.1", ] [[package]] @@ -975,7 +1023,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] @@ -1018,16 +1066,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2 0.4.12", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", "httparse", "itoa", @@ -1044,8 +1092,8 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http 1.3.1", + "hyper 1.8.0", "hyper-util", "rustls", "rustls-native-certs", @@ -1057,18 +1105,18 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.18" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.3.1", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.8.0", "ipnet", "libc", "percent-encoding", @@ -1183,9 +1231,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown", @@ -1199,6 +1247,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1217,9 +1274,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -1233,9 +1290,19 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] [[package]] name = "litemap" @@ -1254,9 +1321,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "matchers" @@ -1478,6 +1545,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -1550,6 +1627,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.13" @@ -1606,6 +1695,12 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1643,9 +1738,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "zeroize", ] @@ -1810,9 +1905,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -1883,9 +1978,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -2062,9 +2157,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2073,9 +2168,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.31" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -2084,9 +2179,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -2115,9 +2210,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", @@ -2205,9 +2300,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.19.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -2258,9 +2353,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -2271,9 +2366,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2281,9 +2376,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ "bumpalo", "proc-macro2", @@ -2294,9 +2389,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -2512,18 +2607,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/Cargo.toml b/aws/rust-runtime/aws-sdk-cloudfront-signer/Cargo.toml new file mode 100644 index 00000000000..f4c21800607 --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "aws-sdk-cloudfront-signer" +version = "0.1.0" +authors = ["AWS Rust SDK Team "] +description = "CloudFront URL and cookie signing utilities for AWS SDK for Rust" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/smithy-lang/smithy-rs" +rust-version = "1.88" + +[dependencies] +aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" } +aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" } +rsa = "0.9.9" +sha1 = { version = "0.10.6", features = ["oid"] } +tokio = { version = "1.40.0", features = ["fs"], optional = true } +base64-simd = "0.8.0" +aws-smithy-json = { path = "../../../rust-runtime/aws-smithy-json" } +p256 = { version = "0.13.2", features = ["ecdsa", "pem"] } +url = "2.5.4" +http = { version = "1.1", optional = true } + +[dev-dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[features] +rt-tokio = ["dep:tokio"] +http-1x = ["dep:http"] + +[package.metadata.docs.rs] +all-features = true +targets = ["x86_64-unknown-linux-gnu"] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +rustdoc-args = ["--cfg", "docsrs"] +# End of docs.rs metadata + +[package.metadata.smithy-rs-release-tooling] +stable = false diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/LICENSE b/aws/rust-runtime/aws-sdk-cloudfront-signer/LICENSE new file mode 100644 index 00000000000..67db8588217 --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/LICENSE @@ -0,0 +1,175 @@ + + 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. diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/README.md b/aws/rust-runtime/aws-sdk-cloudfront-signer/README.md new file mode 100644 index 00000000000..aebc9b70553 --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/README.md @@ -0,0 +1,329 @@ +# aws-sdk-cloudfront-signer + +A library for generating signed URLs and cookies for Amazon CloudFront private content. + +This crate provides utilities to create cryptographically signed URLs and cookies that grant +time-limited access to private CloudFront distributions. It supports both RSA-SHA1 and +ECDSA-SHA1 signing algorithms with canned (simple) and custom (advanced) policies. + +## Key Features + +- **Signed URLs**: Generate URLs with embedded signatures for single-resource access +- **Signed Cookies**: Generate cookies for multi-resource access without URL modification +- **Canned Policies**: Simple time-based expiration +- **Custom Policies**: Advanced access control with activation dates, IP restrictions, and wildcards +- **Multiple Key Formats**: RSA (PKCS#1/PKCS#8) and ECDSA P-256 (PKCS#8) private keys + +## When to Use Signed URLs vs Cookies + +**Use signed URLs when:** +- Restricting access to individual files (e.g., a download link) +- Users are accessing content through a client that doesn't support cookies +- You want to share a link that works without additional setup + +**Use signed cookies when:** +- Providing access to multiple restricted files (e.g., all files in a subscriber area) +- You don't want to change your existing URLs +- You're building a web application where cookies are naturally handled + +## Feature Flags + +| Feature | Description | +|---------|-------------| +| `rt-tokio` | Enables async file loading with `PrivateKey::from_pem_file()` | +| `http-1x` | Enables conversion to `http::Request` types | + +## CloudFront Setup + +Before using this library, you need to: + +1. Create a CloudFront key pair in the AWS Console or via CLI +2. Upload the public key to CloudFront +3. Create a key group containing your public key +4. Configure your CloudFront distribution to use the key group for restricted content +5. Keep the private key secure for use with this library + +See the [CloudFront Developer Guide](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html) for detailed setup instructions. + +## Basic Usage + +### Signing a URL with Canned Policy + +Canned policies provide simple time-based access control with just an expiration time: + +```rust,ignore +use aws_sdk_cloudfront_signer::{sign_url, SigningRequest, PrivateKey}; +use aws_smithy_types::DateTime; + +// Load your CloudFront private key +let private_key = PrivateKey::from_pem(include_bytes!("private_key.pem"))?; + +// Create a signing request +let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg") + .key_pair_id("APKAEIBAERJR2EXAMPLE") + .private_key(private_key) + .expires_at(DateTime::from_secs(1767290400)) // Absolute expiration + .build()?; + +// Generate the signed URL +let signed_url = sign_url(&request)?; +println!("Signed URL: {}", signed_url); +``` + +The resulting URL will include `Expires`, `Signature`, and `Key-Pair-Id` query parameters. + +### Using Relative Expiration + +Instead of an absolute timestamp, you can specify a duration from now: + +```rust,ignore +use std::time::Duration; + +let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/video.mp4") + .key_pair_id("APKAEIBAERJR2EXAMPLE") + .private_key(private_key) + .expires_in(Duration::from_secs(3600)) // Valid for 1 hour + .build()?; +``` + +### Signing a URL with Custom Policy + +Custom policies enable advanced access control with activation dates, IP restrictions, and wildcard patterns: + +```rust,ignore +use aws_sdk_cloudfront_signer::{sign_url, SigningRequest, PrivateKey}; +use aws_smithy_types::DateTime; + +let private_key = PrivateKey::from_pem(include_bytes!("private_key.pem"))?; + +let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/videos/*") + .key_pair_id("APKAEIBAERJR2EXAMPLE") + .private_key(private_key) + .expires_at(DateTime::from_secs(1767290400)) + .active_at(DateTime::from_secs(1767200000)) // Not valid before this time + .ip_range("192.0.2.0/24") // Restrict to IP range + .build()?; + +let signed_url = sign_url(&request)?; +``` + +Custom policy URLs include a `Policy` parameter (base64-encoded JSON) instead of `Expires`. + +### Generating Signed Cookies + +Signed cookies work similarly but return cookie name-value pairs: + +```rust,ignore +use aws_sdk_cloudfront_signer::{sign_cookies, SigningRequest, PrivateKey}; +use aws_smithy_types::DateTime; + +let private_key = PrivateKey::from_pem(include_bytes!("private_key.pem"))?; + +let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/*") + .key_pair_id("APKAEIBAERJR2EXAMPLE") + .private_key(private_key) + .expires_at(DateTime::from_secs(1767290400)) + .build()?; + +let cookies = sign_cookies(&request)?; + +// Set cookies in your HTTP response +for (name, value) in cookies.iter() { + println!("Set-Cookie: {}={}; Domain=d111111abcdef8.cloudfront.net; Secure; HttpOnly", name, value); +} +``` + +For canned policies, cookies include: +- `CloudFront-Expires` +- `CloudFront-Signature` +- `CloudFront-Key-Pair-Id` + +For custom policies, cookies include: +- `CloudFront-Policy` +- `CloudFront-Signature` +- `CloudFront-Key-Pair-Id` + +## Private Key Loading + +### From PEM Bytes + +Load a key from bytes (useful when loading from AWS Secrets Manager or environment variables): + +```rust,ignore +use aws_sdk_cloudfront_signer::PrivateKey; + +// From a byte slice +let key = PrivateKey::from_pem(include_bytes!("private_key.pem"))?; + +// From a string +let pem_string = std::fs::read_to_string("private_key.pem")?; +let key = PrivateKey::from_pem(pem_string.as_bytes())?; +``` + +### From File (Async) + +With the `rt-tokio` feature enabled, you can load keys directly from files: + +```rust,ignore +use aws_sdk_cloudfront_signer::PrivateKey; + +let key = PrivateKey::from_pem_file("private_key.pem").await?; +``` + +### Supported Key Formats + +| Format | Header | Key Type | +|--------|--------|----------| +| PKCS#1 | `-----BEGIN RSA PRIVATE KEY-----` | RSA only | +| PKCS#8 | `-----BEGIN PRIVATE KEY-----` | RSA or ECDSA P-256 | + +Both RSA and ECDSA keys use SHA-1 signatures (required by CloudFront). + +## Policy Types + +### Canned Policy + +A canned policy is automatically used when you only specify an expiration time. It's simpler and produces shorter URLs: + +```rust,ignore +let request = SigningRequest::builder() + .resource_url("https://example.cloudfront.net/file.pdf") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build()?; +``` + +### Custom Policy + +A custom policy is used when you specify any of: +- `resource_pattern()` - Wildcard pattern for the policy (different from the signed URL) +- `active_at()` - URL becomes valid at this time (not-before) +- `ip_range()` - Restrict access to an IPv4 CIDR range + +```rust,ignore +let request = SigningRequest::builder() + .resource_url("https://example.cloudfront.net/premium/video.mp4") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .active_at(DateTime::from_secs(1767200000)) // Triggers custom policy + .ip_range("10.0.0.0/8") // Also triggers custom policy + .build()?; +``` + +## Wildcard Patterns + +Custom policies support wildcards in the resource pattern. Use `resource_pattern()` to specify +a wildcard pattern that grants access to multiple resources, while `resource_url()` specifies +the actual URL being signed: + +- `*` matches zero or more characters +- `?` matches exactly one character + +```rust,ignore +// Sign a specific URL but grant access to all files under /videos/ +let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/videos/intro.mp4") + .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build()?; + +// The signed URL points to intro.mp4, but the policy grants access to all /videos/* +``` + +Common wildcard patterns: +- `https://example.cloudfront.net/videos/*` - All files under /videos/ +- `https://example.cloudfront.net/*.mp4` - All .mp4 files +- `https://example.cloudfront.net/video-?.mp4` - video-1.mp4, video-2.mp4, etc. +- `*` - All resources (use with caution) + +## Using Signed URLs with HTTP Clients + +The `SignedUrl` type provides multiple ways to access the signed URL for use with HTTP clients: + +```rust,ignore +let signed_url = sign_url(&request)?; + +// As a string slice +let url_str: &str = signed_url.as_str(); + +// As a parsed url::Url +let url: &url::Url = signed_url.as_url(); + +// Convert to owned url::Url +let url: url::Url = signed_url.into_url(); + +// Use with reqwest (reqwest re-exports url::Url) +let response = reqwest::get(signed_url.as_url()).await?; + +// Display trait +println!("Signed URL: {}", signed_url); +``` + +### HTTP 1.x Integration + +With the `http-1x` feature enabled, you can convert signed URLs directly to `http::Request`: + +```rust,ignore +use http::Request; + +let signed_url = sign_url(&request)?; + +// Convert to http::Request +let http_request: Request<()> = signed_url.try_into()?; + +// Or from a reference +let http_request: Request<()> = (&signed_url).try_into()?; +``` + +This is useful when working with HTTP clients that use the `http` crate types. +## Error Handling + +All operations return `Result`: + +```rust,ignore +use aws_sdk_cloudfront_signer::{sign_url, SigningRequest, PrivateKey, error::SigningError}; + +let result = sign_url(&request); +match result { + Ok(signed_url) => println!("Success: {}", signed_url), + Err(e) => { + eprintln!("Signing failed: {}", e); + if let Some(source) = e.source() { + eprintln!("Caused by: {}", source); + } + } +} +``` + +Common error scenarios: +- Invalid private key format +- Missing required fields (resource_url, key_pair_id, private_key, expiration) +- Cryptographic signing failures + +## URLs with Existing Query Parameters + +The library correctly handles URLs that already have query parameters: + +```rust,ignore +let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/video.mp4?quality=hd") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build()?; + +let signed_url = sign_url(&request)?; +// Result: https://...?quality=hd&Expires=...&Signature=...&Key-Pair-Id=... +``` + + +This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/smithy-lang/smithy-rs) code generator. + diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/src/error.rs b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/error.rs new file mode 100644 index 00000000000..3240738088b --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/error.rs @@ -0,0 +1,122 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::borrow::Cow; +use std::error::Error as StdError; +use std::fmt; + +#[derive(Debug)] +pub(crate) enum ErrorKind { + InvalidKey, + InvalidPolicy, + InvalidInput, + SigningFailure, +} + +/// Error type for CloudFront signing operations +#[derive(Debug)] +pub struct SigningError { + kind: ErrorKind, + source: Option>, + message: Option>, +} + +impl SigningError { + pub(crate) fn new( + kind: ErrorKind, + source: Option>, + message: Option>, + ) -> Self { + Self { + kind, + source, + message, + } + } + + pub(crate) fn invalid_key(source: impl Into>) -> Self { + Self::new(ErrorKind::InvalidKey, Some(source.into()), None) + } + + pub(crate) fn invalid_policy(message: impl Into>) -> Self { + Self::new(ErrorKind::InvalidPolicy, None, Some(message.into())) + } + + pub(crate) fn invalid_input(message: impl Into>) -> Self { + Self::new(ErrorKind::InvalidInput, None, Some(message.into())) + } + + pub(crate) fn signing_failure(source: impl Into>) -> Self { + Self::new(ErrorKind::SigningFailure, Some(source.into()), None) + } +} + +impl fmt::Display for SigningError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.kind { + ErrorKind::InvalidKey => write!(f, "invalid private key"), + ErrorKind::InvalidPolicy => { + write!(f, "invalid policy")?; + if let Some(ref msg) = self.message { + write!(f, ": {msg}")?; + } + Ok(()) + } + ErrorKind::InvalidInput => { + write!(f, "invalid input")?; + if let Some(ref msg) = self.message { + write!(f, ": {msg}")?; + } + Ok(()) + } + ErrorKind::SigningFailure => write!(f, "signing operation failed"), + } + } +} + +impl StdError for SigningError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.source.as_ref().map(|e| e.as_ref() as _) + } +} + +impl From for SigningError { + fn from(kind: ErrorKind) -> Self { + Self::new(kind, None, None) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invalid_key_display() { + let err = SigningError::invalid_key("test error"); + assert_eq!(err.to_string(), "invalid private key"); + assert!(err.source().is_some()); + } + + #[test] + fn test_invalid_policy_display() { + let err = SigningError::invalid_policy("missing expires_at"); + assert_eq!(err.to_string(), "invalid policy: missing expires_at"); + assert!(err.source().is_none()); + } + + #[test] + fn test_invalid_input_display() { + let err = SigningError::invalid_input("empty URL"); + assert_eq!(err.to_string(), "invalid input: empty URL"); + assert!(err.source().is_none()); + } + + #[test] + fn test_signing_failure_display() { + let err = SigningError::signing_failure("RSA error"); + assert_eq!(err.to_string(), "signing operation failed"); + assert!(err.source().is_some()); + } +} diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/src/key.rs b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/key.rs new file mode 100644 index 00000000000..22d3a933fa0 --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/key.rs @@ -0,0 +1,153 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::error::SigningError; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::RsaPrivateKey; +use sha1::{Digest, Sha1}; + +use p256::ecdsa::signature::SignatureEncoding; +#[cfg(feature = "rt-tokio")] +use std::path::Path; + +/// Private key for signing CloudFront URLs and cookies. +#[derive(Debug, Clone)] +pub enum PrivateKey { + /// RSA private key. + Rsa(Box), + /// ECDSA P-256 private key. + Ecdsa(p256::ecdsa::SigningKey), +} + +impl PrivateKey { + /// Loads a private key from PEM-encoded bytes. + /// + /// Supports RSA keys in PKCS#1 or PKCS#8 format, and ECDSA P-256 keys in PKCS#8 format. + pub fn from_pem(bytes: &[u8]) -> Result { + let pem_str = std::str::from_utf8(bytes).map_err(SigningError::invalid_key)?; + + // Detect key type from PEM header + if pem_str.contains("BEGIN RSA PRIVATE KEY") { + // PKCS#1 RSA format + let key = RsaPrivateKey::from_pkcs1_pem(pem_str).map_err(SigningError::invalid_key)?; + return Ok(PrivateKey::Rsa(Box::new(key))); + } + + if pem_str.contains("BEGIN PRIVATE KEY") { + // PKCS#8 format - could be RSA or ECDSA + // Try ECDSA first (P-256) + if let Ok(key) = p256::ecdsa::SigningKey::from_pkcs8_pem(pem_str) { + return Ok(PrivateKey::Ecdsa(key)); + } + + // Try RSA + use p256::pkcs8::DecodePrivateKey; + let key = RsaPrivateKey::from_pkcs8_pem(pem_str).map_err(SigningError::invalid_key)?; + return Ok(PrivateKey::Rsa(Box::new(key))); + } + + Err(SigningError::invalid_key( + "Unsupported key format. Expected RSA (PKCS#1 or PKCS#8) or ECDSA P-256 (PKCS#8)", + )) + } + + /// Loads a private key from a PEM file asynchronously. + /// + /// Requires the `rt-tokio` feature. + #[cfg(feature = "rt-tokio")] + #[cfg_attr(docsrs, doc(cfg(feature = "rt-tokio")))] + pub async fn from_pem_file(path: impl AsRef) -> Result { + let bytes = tokio::fs::read(path.as_ref()) + .await + .map_err(SigningError::invalid_key)?; + + Self::from_pem(&bytes) + } + + pub(crate) fn sign(&self, message: &[u8]) -> Result, SigningError> { + match self { + PrivateKey::Rsa(key) => { + let mut hasher = Sha1::new(); + hasher.update(message); + let digest = hasher.finalize(); + + let signature = key + .sign(rsa::Pkcs1v15Sign::new::(), &digest) + .map_err(SigningError::signing_failure)?; + + Ok(signature) + } + PrivateKey::Ecdsa(key) => { + use p256::ecdsa::signature::DigestSigner; + + // CloudFront uses SHA1 for all signature types + let mut hasher = Sha1::new(); + hasher.update(message); + + let (signature, _recovery_id): (p256::ecdsa::Signature, _) = key + .try_sign_digest(hasher) + .map_err(SigningError::signing_failure)?; + + // CloudFront expects DER-encoded signatures + Ok(signature.to_der().to_vec()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_RSA_KEY_PEM: &[u8] = b"-----BEGIN RSA PRIVATE KEY----- +MIIBPAIBAAJBANW8WjQksUoX/7nwOfRDNt1XQpLCueHoXSt91MASMOSAqpbzZvXO +g2hW2gCFUIFUPCByMXPoeRe6iUZ5JtjepssCAwEAAQJBALR7ybwQY/lKTLKJrZab +D4BXCCt/7ZFbMxnftsC+W7UHef4S4qFW8oOOLeYfmyGZK1h44rXf2AIp4PndKUID +1zECIQD1suunYw5U22Pa0+2dFThp1VMXdVbPuf/5k3HT2/hSeQIhAN6yX0aT/N6G +gb1XlBKw6GQvhcM0fXmP+bVXV+RtzFJjAiAP+2Z2yeu5u1egeV6gdCvqPnUcNobC +FmA/NMcXt9xMSQIhALEMMJEFAInNeAIXSYKeoPNdkMPDzGnD3CueuCLEZCevAiEA +j+KnJ7pJkTvOzFwE8RfNLli9jf6/OhyYaLL4et7Ng5k= +-----END RSA PRIVATE KEY-----"; + + const TEST_ECDSA_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4//aTM1/HqiVWagy +01cAx3EaegJ0Y5KLRoTtub8T8EWhRANCAARV/wa477wYpyWB5LCrCdS5M9bEAvD+ +VORtjoydSpheKlsa+gE4PcFG88G2gE1Lilb8f6wEq/Lz+5kFa2S8gZmb +-----END PRIVATE KEY-----"; + + #[test] + fn test_from_pem_invalid() { + let result = PrivateKey::from_pem(b"invalid pem data"); + assert!(result.is_err()); + } + + #[test] + fn test_rsa_key_parsing() { + let key = PrivateKey::from_pem(TEST_RSA_KEY_PEM).expect("valid RSA key"); + assert!(matches!(key, PrivateKey::Rsa(_))); + } + + #[test] + fn test_ecdsa_key_parsing() { + let key = PrivateKey::from_pem(TEST_ECDSA_KEY_PEM).expect("valid ECDSA key"); + assert!(matches!(key, PrivateKey::Ecdsa(_))); + } + + #[test] + fn test_rsa_sign() { + let key = PrivateKey::from_pem(TEST_RSA_KEY_PEM).expect("valid test key"); + let message = b"test message"; + let signature = key.sign(message).expect("signing should succeed"); + assert!(!signature.is_empty()); + } + + #[test] + fn test_ecdsa_sign() { + let key = PrivateKey::from_pem(TEST_ECDSA_KEY_PEM).expect("valid test key"); + let message = b"test message"; + let signature = key.sign(message).expect("signing should succeed"); + assert!(!signature.is_empty()); + } +} diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/src/lib.rs b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/lib.rs new file mode 100644 index 00000000000..e0ff0b31e57 --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/lib.rs @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Automatically managed default lints */ +#![cfg_attr(docsrs, feature(doc_cfg))] +/* End of automatically managed default lints */ +#![doc = include_str!("../README.md")] +#![warn( + missing_docs, + rustdoc::missing_crate_level_docs, + missing_debug_implementations, + rust_2018_idioms, + unreachable_pub +)] + +/// Error types for CloudFront signing operations. +pub mod error; +mod key; +mod policy; +mod sign; + +pub use key::PrivateKey; +pub use sign::{SignedCookies, SignedUrl, SigningRequest, SigningRequestBuilder}; + +/// Sign a CloudFront URL with canned or custom policy +pub fn sign_url(request: &SigningRequest) -> Result { + request.sign_url() +} + +/// Generate signed cookies with canned or custom policy +pub fn sign_cookies(request: &SigningRequest) -> Result { + request.sign_cookies() +} diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/src/policy.rs b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/policy.rs new file mode 100644 index 00000000000..504b131d26b --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/policy.rs @@ -0,0 +1,215 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::error::SigningError; +use aws_smithy_types::{DateTime, Number}; + +#[derive(Debug, Clone)] +pub(crate) struct Policy { + resource: String, + date_less_than: DateTime, + date_greater_than: Option, + ip_address: Option, +} + +impl Policy { + pub(crate) fn builder() -> PolicyBuilder { + PolicyBuilder::default() + } + + pub(crate) fn to_json(&self) -> String { + let mut out = String::new(); + let mut root = aws_smithy_json::serialize::JsonObjectWriter::new(&mut out); + + let mut statement_array = root.key("Statement").start_array(); + let mut statement = statement_array.value().start_object(); + statement.key("Resource").string(&self.resource); + + let mut condition = statement.key("Condition").start_object(); + + let mut date_less = condition.key("DateLessThan").start_object(); + date_less + .key("AWS:EpochTime") + .number(Number::PosInt(self.date_less_than.secs() as u64)); + date_less.finish(); + + if let Some(starts) = self.date_greater_than { + let mut date_greater = condition.key("DateGreaterThan").start_object(); + date_greater + .key("AWS:EpochTime") + .number(Number::PosInt(starts.secs() as u64)); + date_greater.finish(); + } + + if let Some(ref ip) = self.ip_address { + let mut ip_addr = condition.key("IpAddress").start_object(); + ip_addr.key("AWS:SourceIp").string(ip); + ip_addr.finish(); + } + + condition.finish(); + statement.finish(); + statement_array.finish(); + root.finish(); + + out + } + + pub(crate) fn to_cloudfront_base64(&self) -> String { + let json = self.to_json(); + base64_simd::STANDARD + .encode_to_string(json.as_bytes()) + .replace('+', "-") + .replace('=', "_") + .replace('/', "~") + } +} + +#[derive(Default)] +pub(crate) struct PolicyBuilder { + resource: Option, + expires_at: Option, + starts_at: Option, + ip_range: Option, +} + +impl PolicyBuilder { + pub(crate) fn resource(mut self, url: impl Into) -> Self { + self.resource = Some(url.into()); + self + } + + pub(crate) fn expires_at(mut self, time: DateTime) -> Self { + self.expires_at = Some(time); + self + } + + pub(crate) fn starts_at(mut self, time: DateTime) -> Self { + self.starts_at = Some(time); + self + } + + pub(crate) fn ip_range(mut self, cidr: impl Into) -> Self { + self.ip_range = Some(cidr.into()); + self + } + + pub(crate) fn build(self) -> Result { + let resource = self + .resource + .ok_or_else(|| SigningError::invalid_policy("resource is required"))?; + + let date_less_than = self + .expires_at + .ok_or_else(|| SigningError::invalid_policy("expires_at is required"))?; + + if let Some(starts) = self.starts_at { + if starts.secs() >= date_less_than.secs() { + return Err(SigningError::invalid_policy( + "starts_at must be before expires_at", + )); + } + } + + Ok(Policy { + resource, + date_less_than, + date_greater_than: self.starts_at, + ip_address: self.ip_range, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_canned_policy() { + let policy = Policy::builder() + .resource("https://d111111abcdef8.cloudfront.net/image.jpg") + .expires_at(DateTime::from_secs(1767290400)) + .build() + .expect("valid canned policy"); + + let json = policy.to_json(); + assert!(json.contains("\"Resource\":\"https://d111111abcdef8.cloudfront.net/image.jpg\"")); + assert!(json.contains("\"AWS:EpochTime\":1767290400")); + assert!(!json.contains("DateGreaterThan")); + assert!(!json.contains("IpAddress")); + } + + #[test] + fn test_custom_policy_with_starts_at() { + let policy = Policy::builder() + .resource("https://d111111abcdef8.cloudfront.net/*") + .expires_at(DateTime::from_secs(1767290400)) + .starts_at(DateTime::from_secs(1767200000)) + .build() + .expect("valid custom policy"); + + let json = policy.to_json(); + assert!(json.contains("DateGreaterThan")); + assert!(json.contains("\"AWS:EpochTime\":1767200000")); + } + + #[test] + fn test_custom_policy_with_ip_range() { + let policy = Policy::builder() + .resource("https://d111111abcdef8.cloudfront.net/video.mp4") + .expires_at(DateTime::from_secs(1767290400)) + .ip_range("192.0.2.0/24") + .build() + .expect("valid custom policy"); + + let json = policy.to_json(); + assert!(json.contains("IpAddress")); + assert!(json.contains("\"AWS:SourceIp\":\"192.0.2.0/24\"")); + } + + #[test] + fn test_missing_resource() { + let result = Policy::builder() + .expires_at(DateTime::from_secs(1767290400)) + .build(); + + assert!(result.is_err()); + } + + #[test] + fn test_missing_expires_at() { + let result = Policy::builder() + .resource("https://example.com/file.txt") + .build(); + + assert!(result.is_err()); + } + + #[test] + fn test_starts_at_after_expires_at() { + let result = Policy::builder() + .resource("https://example.com/file.txt") + .expires_at(DateTime::from_secs(1767200000)) + .starts_at(DateTime::from_secs(1767290400)) + .build(); + + assert!(result.is_err()); + } + + #[test] + fn test_cloudfront_base64_encoding() { + let policy = Policy::builder() + .resource("https://example.com/test") + .expires_at(DateTime::from_secs(1767290400)) + .build() + .expect("valid policy"); + + let encoded = policy.to_cloudfront_base64(); + // CloudFront encoding uses ~ for / and _ for = + assert!(!encoded.contains('+')); + assert!(!encoded.contains('/')); + assert!(!encoded.contains('=')); + } +} diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/src/sign.rs b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/sign.rs new file mode 100644 index 00000000000..3fc29f5b269 --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/src/sign.rs @@ -0,0 +1,593 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::error::{ErrorKind, SigningError}; +use crate::key::PrivateKey; +use crate::policy::Policy; +use aws_smithy_types::DateTime; +use std::borrow::Cow; +use std::fmt; +use std::time::Duration; + +/// CloudFront-specific base64 encoding. +/// Standard base64 with: `+` → `-`, `=` → `_`, `/` → `~` +fn cloudfront_base64(data: &[u8]) -> String { + base64_simd::STANDARD + .encode_to_string(data) + .replace('+', "-") + .replace('=', "_") + .replace('/', "~") +} + +const COOKIE_POLICY: &str = "CloudFront-Policy"; +const COOKIE_SIGNATURE: &str = "CloudFront-Signature"; +const COOKIE_KEY_PAIR_ID: &str = "CloudFront-Key-Pair-Id"; +const COOKIE_EXPIRES: &str = "CloudFront-Expires"; + +#[derive(Debug, Clone)] +enum Expiration { + DateTime(DateTime), + Duration(Duration), +} + +/// Request to sign a CloudFront URL or generate signed cookies. +#[derive(Debug, Clone)] +pub struct SigningRequest { + pub(crate) resource_url: String, + pub(crate) resource_pattern: Option, + pub(crate) key_pair_id: String, + pub(crate) private_key: PrivateKey, + pub(crate) expiration: DateTime, + pub(crate) active_date: Option, + pub(crate) ip_range: Option, +} + +impl SigningRequest { + /// Creates a new builder for constructing a signing request. + pub fn builder() -> SigningRequestBuilder { + SigningRequestBuilder::default() + } +} + +/// Builder for [`SigningRequest`]. +#[derive(Default, Debug)] +pub struct SigningRequestBuilder { + resource_url: Option, + resource_pattern: Option, + key_pair_id: Option, + private_key: Option, + expiration: Option, + active_date: Option, + ip_range: Option, + time_source: Option, +} + +impl SigningRequestBuilder { + /// Sets the CloudFront resource URL to sign. + pub fn resource_url(mut self, url: impl Into) -> Self { + self.resource_url = Some(url.into()); + self + } + + /// Sets a wildcard pattern for the policy's `Resource` field. + /// + /// Use this when you want the signed URL to grant access to multiple resources + /// matching a pattern. If not set, `resource_url` is used in the policy. + /// + /// Wildcards: + /// - `*` matches zero or more characters + /// - `?` matches exactly one character + /// + /// # Example + /// ```ignore + /// // Sign a specific URL but grant access to all files under /videos/ + /// SigningRequest::builder() + /// .resource_url("https://d111111abcdef8.cloudfront.net/videos/intro.mp4") + /// .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*") + /// // ... + /// ``` + pub fn resource_pattern(mut self, pattern: impl Into) -> Self { + self.resource_pattern = Some(pattern.into()); + self + } + + /// Sets the CloudFront key pair ID. + pub fn key_pair_id(mut self, id: impl Into) -> Self { + self.key_pair_id = Some(id.into()); + self + } + + /// Sets the private key for signing. + pub fn private_key(mut self, key: PrivateKey) -> Self { + self.private_key = Some(key); + self + } + + /// Sets an absolute expiration time. + pub fn expires_at(mut self, time: DateTime) -> Self { + self.expiration = Some(Expiration::DateTime(time)); + self + } + + /// Sets a relative expiration time from now. + pub fn expires_in(mut self, duration: Duration) -> Self { + self.expiration = Some(Expiration::Duration(duration)); + self + } + + /// Sets an activation time (not-before date) for custom policy. + pub fn active_at(mut self, time: DateTime) -> Self { + self.active_date = Some(time); + self + } + + /// Sets an IP range restriction (CIDR notation) for custom policy. + pub fn ip_range(mut self, cidr: impl Into) -> Self { + self.ip_range = Some(cidr.into()); + self + } + + /// Builds the signing request. + pub fn build(self) -> Result { + let resource_url = self + .resource_url + .ok_or_else(|| SigningError::invalid_input("resource_url is required"))?; + + let key_pair_id = self + .key_pair_id + .ok_or_else(|| SigningError::invalid_input("key_pair_id is required"))?; + + let private_key = self + .private_key + .ok_or_else(|| SigningError::invalid_input("private_key is required"))?; + + let expiration = self.expiration.ok_or_else(|| { + SigningError::invalid_input("expiration is required (use expires_at or expires_in)") + })?; + + let expiration = match expiration { + Expiration::DateTime(dt) => dt, + Expiration::Duration(dur) => { + let time_source = self.time_source.unwrap_or_default(); + let now = DateTime::from(time_source.now()); + DateTime::from_secs(now.secs() + dur.as_secs() as i64) + } + }; + + // Validate that activeDate is before expirationDate + if let Some(active) = self.active_date { + if active.secs() >= expiration.secs() { + return Err(SigningError::invalid_input( + "active_at must be before expiration", + )); + } + } + + Ok(SigningRequest { + resource_url, + resource_pattern: self.resource_pattern, + key_pair_id, + private_key, + expiration, + active_date: self.active_date, + ip_range: self.ip_range, + }) + } +} + +/// A signed CloudFront URL. +#[derive(Debug, Clone)] +pub struct SignedUrl { + url: url::Url, +} + +impl SignedUrl { + pub(crate) fn new(url: String) -> Result { + let url = url::Url::parse(&url).map_err(|e| { + SigningError::new( + ErrorKind::InvalidInput, + Some(Box::new(e)), + Some("failed to parse URL".into()), + ) + })?; + Ok(Self { url }) + } + + /// Returns the complete signed URL as a string. + pub fn as_str(&self) -> &str { + self.url.as_str() + } + + /// Returns a reference to the parsed URL. + pub fn as_url(&self) -> &url::Url { + &self.url + } + + /// Consumes self and returns the parsed URL. + pub fn into_url(self) -> url::Url { + self.url + } +} + +impl fmt::Display for SignedUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.url) + } +} + +impl AsRef for SignedUrl { + fn as_ref(&self) -> &str { + self.url.as_str() + } +} + +impl AsRef for SignedUrl { + fn as_ref(&self) -> &url::Url { + &self.url + } +} + +#[cfg(feature = "http-1x")] +impl TryFrom for http::Request<()> { + type Error = http::Error; + + fn try_from(signed_url: SignedUrl) -> Result { + http::Request::builder() + .uri(signed_url.url.as_str()) + .body(()) + } +} + +#[cfg(feature = "http-1x")] +impl TryFrom<&SignedUrl> for http::Request<()> { + type Error = http::Error; + + fn try_from(signed_url: &SignedUrl) -> Result { + http::Request::builder() + .uri(signed_url.url.as_str()) + .body(()) + } +} + +/// Signed cookies for CloudFront. +#[derive(Debug, Clone)] +pub struct SignedCookies { + cookies: Vec<(Cow<'static, str>, String)>, +} + +impl SignedCookies { + pub(crate) fn new(cookies: Vec<(Cow<'static, str>, String)>) -> Self { + Self { cookies } + } + + /// Returns all cookies as name-value pairs. + pub fn cookies(&self) -> &[(Cow<'static, str>, String)] { + &self.cookies + } + + /// Gets a specific cookie value by name. + pub fn get(&self, name: &str) -> Option<&str> { + self.cookies + .iter() + .find(|(n, _)| n == name) + .map(|(_, v)| v.as_str()) + } + + /// Returns an iterator over cookies. + pub fn iter(&self) -> impl Iterator { + self.cookies.iter().map(|(n, v)| (n.as_ref(), v.as_str())) + } +} + +// Internal signing implementation +impl SigningRequest { + /// Returns true if this request should use a canned policy. + /// Canned policy is used only when there's no resource_pattern, active_date, or ip_range. + fn use_canned_policy(&self) -> bool { + self.resource_pattern.is_none() && self.active_date.is_none() && self.ip_range.is_none() + } + + pub(crate) fn sign_url(&self) -> Result { + let policy = self.build_policy()?; + let policy_json = policy.to_json(); + let signature = self.private_key.sign(policy_json.as_bytes())?; + let signature_b64 = cloudfront_base64(&signature); + + let separator = if self.resource_url.contains('?') { + "&" + } else { + "?" + }; + + let signed_url = if self.use_canned_policy() { + format!( + "{}{}Expires={}&Signature={}&Key-Pair-Id={}", + self.resource_url, + separator, + self.expiration.secs(), + signature_b64, + self.key_pair_id + ) + } else { + let policy_b64 = policy.to_cloudfront_base64(); + format!( + "{}{}Policy={}&Signature={}&Key-Pair-Id={}", + self.resource_url, separator, policy_b64, signature_b64, self.key_pair_id + ) + }; + + SignedUrl::new(signed_url) + } + + pub(crate) fn sign_cookies(&self) -> Result { + let policy = self.build_policy()?; + let policy_json = policy.to_json(); + let signature = self.private_key.sign(policy_json.as_bytes())?; + let signature_b64 = cloudfront_base64(&signature); + + let cookies = if self.use_canned_policy() { + vec![ + ( + Cow::Borrowed(COOKIE_EXPIRES), + self.expiration.secs().to_string(), + ), + (Cow::Borrowed(COOKIE_SIGNATURE), signature_b64), + (Cow::Borrowed(COOKIE_KEY_PAIR_ID), self.key_pair_id.clone()), + ] + } else { + let policy_b64 = policy.to_cloudfront_base64(); + vec![ + (Cow::Borrowed(COOKIE_POLICY), policy_b64), + (Cow::Borrowed(COOKIE_SIGNATURE), signature_b64), + (Cow::Borrowed(COOKIE_KEY_PAIR_ID), self.key_pair_id.clone()), + ] + }; + + Ok(SignedCookies::new(cookies)) + } + + fn build_policy(&self) -> Result { + // Use resource_pattern for policy if set, otherwise use resource_url + let policy_resource = self.resource_pattern.as_ref().unwrap_or(&self.resource_url); + + let mut builder = Policy::builder() + .resource(policy_resource) + .expires_at(self.expiration); + + if let Some(active) = self.active_date { + builder = builder.starts_at(active); + } + if let Some(ref ip) = self.ip_range { + builder = builder.ip_range(ip); + } + + builder.build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_RSA_KEY: &[u8] = b"-----BEGIN RSA PRIVATE KEY----- +MIIBPAIBAAJBANW8WjQksUoX/7nwOfRDNt1XQpLCueHoXSt91MASMOSAqpbzZvXO +g2hW2gCFUIFUPCByMXPoeRe6iUZ5JtjepssCAwEAAQJBALR7ybwQY/lKTLKJrZab +D4BXCCt/7ZFbMxnftsC+W7UHef4S4qFW8oOOLeYfmyGZK1h44rXf2AIp4PndKUID +1zECIQD1suunYw5U22Pa0+2dFThp1VMXdVbPuf/5k3HT2/hSeQIhAN6yX0aT/N6G +gb1XlBKw6GQvhcM0fXmP+bVXV+RtzFJjAiAP+2Z2yeu5u1egeV6gdCvqPnUcNobC +FmA/NMcXt9xMSQIhALEMMJEFAInNeAIXSYKeoPNdkMPDzGnD3CueuCLEZCevAiEA +j+KnJ7pJkTvOzFwE8RfNLli9jf6/OhyYaLL4et7Ng5k= +-----END RSA PRIVATE KEY-----"; + + #[test] + fn test_sign_url_canned_policy() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let signed_url = request.sign_url().unwrap(); + let url = signed_url.as_str(); + + assert!(url.contains("Expires=1767290400")); + assert!(url.contains("Signature=")); + assert!(url.contains("Key-Pair-Id=APKAEXAMPLE")); + assert!(!url.contains("Policy=")); + } + + #[test] + fn test_sign_url_custom_policy() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/*") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .active_at(DateTime::from_secs(1767200000)) + .build() + .unwrap(); + + let signed_url = request.sign_url().unwrap(); + let url = signed_url.as_str(); + + assert!(url.contains("Policy=")); + assert!(url.contains("Signature=")); + assert!(url.contains("Key-Pair-Id=APKAEXAMPLE")); + assert!(!url.contains("Expires=")); + } + + #[test] + fn test_sign_url_with_existing_params() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg?size=large") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let signed_url = request.sign_url().unwrap(); + let url = signed_url.as_str(); + + assert!(url.contains("size=large")); + assert!(url.contains("&Expires=")); + } + + #[test] + fn test_sign_cookies_canned_policy() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/*") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let cookies = request.sign_cookies().unwrap(); + + assert_eq!(cookies.get("CloudFront-Expires"), Some("1767290400")); + assert!(cookies.get("CloudFront-Signature").is_some()); + assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE")); + assert!(cookies.get("CloudFront-Policy").is_none()); + } + + #[test] + fn test_sign_cookies_custom_policy() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/*") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .ip_range("192.0.2.0/24") + .build() + .unwrap(); + + let cookies = request.sign_cookies().unwrap(); + + assert!(cookies.get("CloudFront-Policy").is_some()); + assert!(cookies.get("CloudFront-Signature").is_some()); + assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE")); + assert!(cookies.get("CloudFront-Expires").is_none()); + } + + #[test] + fn test_sign_url_with_resource_pattern() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/videos/intro.mp4") + .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let signed_url = request.sign_url().unwrap(); + let url = signed_url.as_str(); + + // Should use custom policy format (Policy param) because resource_pattern is set + assert!(url.contains("Policy=")); + assert!(url.contains("Signature=")); + assert!(url.contains("Key-Pair-Id=APKAEXAMPLE")); + assert!(!url.contains("Expires=")); + // The actual URL should be the resource_url, not the pattern + assert!(url.starts_with("https://d111111abcdef8.cloudfront.net/videos/intro.mp4?")); + } + + #[test] + fn test_sign_cookies_with_resource_pattern() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/videos/*") + .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let cookies = request.sign_cookies().unwrap(); + + // Should use custom policy format because resource_pattern is set + assert!(cookies.get("CloudFront-Policy").is_some()); + assert!(cookies.get("CloudFront-Signature").is_some()); + assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE")); + assert!(cookies.get("CloudFront-Expires").is_none()); + } + + #[test] + fn test_signed_url_accessors() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let signed_url = request.sign_url().unwrap(); + + // Test as_str() + let url_str = signed_url.as_str(); + assert!(url_str.starts_with("https://d111111abcdef8.cloudfront.net/image.jpg")); + assert!(url_str.contains("Expires=")); + + // Test as_url() + let url_ref = signed_url.as_url(); + assert_eq!(url_ref.scheme(), "https"); + assert_eq!(url_ref.host_str(), Some("d111111abcdef8.cloudfront.net")); + assert_eq!(url_ref.path(), "/image.jpg"); + + // Test Display + let displayed = format!("{}", signed_url); + assert_eq!(displayed, url_str); + + // Test AsRef + let as_ref_str: &str = signed_url.as_ref(); + assert_eq!(as_ref_str, url_str); + + // Test AsRef + let as_ref_url: &url::Url = signed_url.as_ref(); + assert_eq!(as_ref_url, url_ref); + + // Test into_url() + let url = signed_url.into_url(); + assert_eq!(url.scheme(), "https"); + assert_eq!(url.host_str(), Some("d111111abcdef8.cloudfront.net")); + } + + #[cfg(feature = "http-1x")] + #[test] + fn test_signed_url_to_http_request() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let signed_url = request.sign_url().unwrap(); + + // Test TryFrom + let http_req: http::Request<()> = signed_url.clone().try_into().unwrap(); + assert_eq!(http_req.method(), http::Method::GET); + assert!(http_req.uri().to_string().contains("Expires=")); + + // Test TryFrom<&SignedUrl> + let http_req: http::Request<()> = (&signed_url).try_into().unwrap(); + assert_eq!(http_req.method(), http::Method::GET); + assert!(http_req.uri().to_string().contains("Expires=")); + } +} diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/integration_test.rs b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/integration_test.rs new file mode 100644 index 00000000000..458473192e6 --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/integration_test.rs @@ -0,0 +1,83 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_sdk_cloudfront_signer::{sign_cookies, sign_url, PrivateKey, SigningRequest}; +use aws_smithy_types::DateTime; + +const TEST_RSA_KEY: &[u8] = b"-----BEGIN RSA PRIVATE KEY----- +MIIBPAIBAAJBANW8WjQksUoX/7nwOfRDNt1XQpLCueHoXSt91MASMOSAqpbzZvXO +g2hW2gCFUIFUPCByMXPoeRe6iUZ5JtjepssCAwEAAQJBALR7ybwQY/lKTLKJrZab +D4BXCCt/7ZFbMxnftsC+W7UHef4S4qFW8oOOLeYfmyGZK1h44rXf2AIp4PndKUID +1zECIQD1suunYw5U22Pa0+2dFThp1VMXdVbPuf/5k3HT2/hSeQIhAN6yX0aT/N6G +gb1XlBKw6GQvhcM0fXmP+bVXV+RtzFJjAiAP+2Z2yeu5u1egeV6gdCvqPnUcNobC +FmA/NMcXt9xMSQIhALEMMJEFAInNeAIXSYKeoPNdkMPDzGnD3CueuCLEZCevAiEA +j+KnJ7pJkTvOzFwE8RfNLli9jf6/OhyYaLL4et7Ng5k= +-----END RSA PRIVATE KEY-----"; + +const TEST_ECDSA_KEY: &[u8] = b"-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg4//aTM1/HqiVWagy +01cAx3EaegJ0Y5KLRoTtub8T8EWhRANCAARV/wa477wYpyWB5LCrCdS5M9bEAvD+ +VORtjoydSpheKlsa+gE4PcFG88G2gE1Lilb8f6wEq/Lz+5kFa2S8gZmb +-----END PRIVATE KEY-----"; + +#[test] +fn test_sign_url_with_rsa_key() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let signed_url = sign_url(&request).unwrap(); + assert!(signed_url.as_str().contains("Signature=")); +} + +#[test] +fn test_sign_url_with_ecdsa_key() { + let key = PrivateKey::from_pem(TEST_ECDSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let signed_url = sign_url(&request).unwrap(); + assert!(signed_url.as_str().contains("Signature=")); +} + +#[test] +fn test_sign_cookies_with_rsa_key() { + let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/*") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let cookies = sign_cookies(&request).unwrap(); + assert!(cookies.get("CloudFront-Signature").is_some()); +} + +#[test] +fn test_sign_cookies_with_ecdsa_key() { + let key = PrivateKey::from_pem(TEST_ECDSA_KEY).unwrap(); + let request = SigningRequest::builder() + .resource_url("https://d111111abcdef8.cloudfront.net/*") + .key_pair_id("APKAEXAMPLE") + .private_key(key) + .expires_at(DateTime::from_secs(1767290400)) + .build() + .unwrap(); + + let cookies = sign_cookies(&request).unwrap(); + assert!(cookies.get("CloudFront-Signature").is_some()); +} diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/sep_test_cases.rs b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/sep_test_cases.rs new file mode 100644 index 00000000000..161571ac57a --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/sep_test_cases.rs @@ -0,0 +1,213 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_sdk_cloudfront_signer::{sign_cookies, sign_url, PrivateKey, SigningRequest}; +use aws_smithy_types::DateTime; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Deserialize)] +struct TestCase { + id: String, + documentation: String, + input: TestInput, + expected: TestExpected, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TestInput { + resource_url: String, + key_pair_id: String, + private_key_file: String, + expiration_date: Option, + active_date: Option, + ip_range: Option, + resource_url_pattern: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TestExpected { + #[allow(dead_code)] + policy_json: Option, + query_params: Option>, + cookies: Option>, + signature: Option, + signature_algorithm: Option, + error: Option, + error_contains: Option>, +} + +#[derive(Debug, Deserialize)] +struct TestCases { + #[serde(flatten)] + #[allow(dead_code)] + cases: Vec, +} + +fn load_test_cases() -> Vec { + let json = include_str!("test-cases.json"); + serde_json::from_str(json).expect("Failed to parse test cases") +} + +fn parse_url_query_params(url: &str) -> HashMap { + url.split('?') + .nth(1) + .map(|query| { + query + .split('&') + .filter_map(|pair| { + let mut parts = pair.split('='); + Some((parts.next()?.to_string(), parts.next()?.to_string())) + }) + .collect() + }) + .unwrap_or_default() +} + +#[test] +fn test_sep_test_cases() { + let test_cases = load_test_cases(); + + for test_case in test_cases { + println!( + "\nRunning test: {} - {}", + test_case.id, test_case.documentation + ); + + // Load private key + let key_path = format!("tests/{}", test_case.input.private_key_file); + let key_bytes = std::fs::read(&key_path) + .unwrap_or_else(|_| panic!("Failed to read key file: {key_path}")); + let private_key = PrivateKey::from_pem(&key_bytes) + .unwrap_or_else(|_| panic!("Failed to parse private key for test {}", test_case.id)); + + // Build signing request + let mut builder = SigningRequest::builder() + .resource_url(&test_case.input.resource_url) + .key_pair_id(&test_case.input.key_pair_id) + .private_key(private_key); + + if let Some(exp) = test_case.input.expiration_date { + builder = builder.expires_at(DateTime::from_secs(exp)); + } + + if let Some(active) = test_case.input.active_date { + builder = builder.active_at(DateTime::from_secs(active)); + } + + if let Some(ip) = &test_case.input.ip_range { + builder = builder.ip_range(ip); + } + + if let Some(pattern) = &test_case.input.resource_url_pattern { + builder = builder.resource_pattern(pattern); + } + + // Handle error cases + if test_case.expected.error == Some(true) { + let result = builder.build(); + assert!( + result.is_err(), + "Test {} expected error but succeeded", + test_case.id + ); + + if let Some(error_contains) = &test_case.expected.error_contains { + let error_msg = result.unwrap_err().to_string().to_lowercase(); + for expected_text in error_contains { + assert!( + error_msg.contains(&expected_text.to_lowercase()), + "Test {} error message '{}' does not contain '{}'", + test_case.id, + error_msg, + expected_text + ); + } + } + continue; + } + + let request = builder + .build() + .unwrap_or_else(|e| panic!("Failed to build request for test {}: {}", test_case.id, e)); + + // Determine if this is a URL or cookie test + let is_cookie_test = test_case.expected.cookies.is_some(); + + if is_cookie_test { + // Test signed cookies + let cookies = sign_cookies(&request).unwrap_or_else(|e| { + panic!("Failed to sign cookies for test {}: {}", test_case.id, e) + }); + + let cookie_map: HashMap = cookies + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + // Verify expected cookies + if let Some(expected_cookies) = &test_case.expected.cookies { + for (key, expected_value) in expected_cookies { + let actual_value = cookie_map + .get(key) + .unwrap_or_else(|| panic!("Test {} missing cookie: {}", test_case.id, key)); + + assert_eq!( + actual_value, expected_value, + "Test {} cookie {} mismatch", + test_case.id, key + ); + } + } + } else { + // Test signed URL + let signed_url = sign_url(&request) + .unwrap_or_else(|e| panic!("Failed to sign URL for test {}: {}", test_case.id, e)); + + let query_params = parse_url_query_params(signed_url.as_str()); + + // Verify expected query parameters + if let Some(expected_params) = &test_case.expected.query_params { + for (key, expected_value) in expected_params { + let actual_value = query_params.get(key).unwrap_or_else(|| { + panic!("Test {} missing query param: {}", test_case.id, key) + }); + + assert_eq!( + actual_value, expected_value, + "Test {} query param {} mismatch", + test_case.id, key + ); + } + } + + // Verify signature if provided (skip for ECDSA as it may not be deterministic) + if let Some(expected_signature) = &test_case.expected.signature { + if test_case.expected.signature_algorithm.as_deref() != Some("ECDSA-SHA1") { + let actual_signature = query_params.get("Signature").unwrap_or_else(|| { + panic!("Test {} missing Signature query param", test_case.id) + }); + + assert_eq!( + actual_signature, expected_signature, + "Test {} signature mismatch", + test_case.id + ); + } else { + // For ECDSA, just verify signature is present + assert!( + query_params.contains_key("Signature"), + "Test {} missing Signature query param", + test_case.id + ); + } + } + } + + println!("✓ Test {} passed", test_case.id); + } +} diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-cases.json b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-cases.json new file mode 100644 index 00000000000..3267fd2cad6 --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-cases.json @@ -0,0 +1,184 @@ +[ + { + "id": "canned-policy-url-basic", + "documentation": "Sign a URL with canned policy (expiration only)", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/image.jpg", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem", + "expirationDate": 1767290400 + }, + "expected": { + "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/image.jpg\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}", + "queryParams": { + "Expires": "1767290400", + "Key-Pair-Id": "K1TESTKEY" + }, + "signature": "iONoMLnhiCy9q1~WB9GkR2DiHz18I85i3o6kZ64REf-fCSOg-AyXEZiq7fJuS~DT-kbZXjVpgIQqI4sCTcBW9XpO6dyJ5sh8Igk3V~OVncS9acGVnI~ZhHBWiGhU8GmkEMAhn6R2RGO-wKGClrXdJGEUE26XoALdHUzHbmU6AGI_", + "signatureAlgorithm": "RSA-SHA1" + } + }, + { + "id": "canned-policy-url-existing-params", + "documentation": "Sign a URL that already has query parameters", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/image.jpg?size=large", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem", + "expirationDate": 1767290400 + }, + "expected": { + "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/image.jpg?size=large\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}", + "queryParams": { + "size": "large", + "Expires": "1767290400", + "Key-Pair-Id": "K1TESTKEY" + }, + "signature": "kx5dxV5cSW50EuCMtYi9JI0D5H97if~zNqwKxZtS7FPc3NV5oi~zCb45ti8ve-YDYw45hzN4lceF6zhW~STVWa7r7fC9wi5XBG1T6ZT2b9R0wMWmGxsy9uVGXFn6JqcjQMsq09MnWrPezK0qFQ2X6pV1nGQqLQZ2sgO~FSlEoCo_", + "signatureAlgorithm": "RSA-SHA1" + } + }, + { + "id": "custom-policy-url-with-active-date", + "documentation": "Sign a URL with custom policy including activation date", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/video.mp4", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem", + "expirationDate": 1767290400, + "activeDate": 1767200000 + }, + "expected": { + "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/video.mp4\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400},\"DateGreaterThan\":{\"AWS:EpochTime\":1767200000}}}]}", + "queryParams": { + "Key-Pair-Id": "K1TESTKEY" + }, + "signature": "aF0WSOV6dkNn-D0HJ~-EwLG69-4QZZ9dv1PgAZEsER~WXkaNa1pBM2aTVXrskBWVHUU6hc1nD6ZKYgBS5Sb9JluFVT0MbQWR~DrtUGu8ugCsnfzUe6ov38nFPBwQICY~jnoQZEXxO2ZkQrluOO4~jWOpqAGrCw5tkdHAnb9gHYQ_", + "signatureAlgorithm": "RSA-SHA1" + } + }, + { + "id": "custom-policy-url-with-ip-range", + "documentation": "Sign a URL with custom policy including IP restriction", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/document.pdf", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem", + "expirationDate": 1767290400, + "ipRange": "192.168.0.0/24" + }, + "expected": { + "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/document.pdf\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400},\"IpAddress\":{\"AWS:SourceIp\":\"192.168.0.0/24\"}}}]}", + "queryParams": { + "Key-Pair-Id": "K1TESTKEY" + }, + "signature": "fq~vePR~kmc5kWDveZOJ3j6PSYRsHSSanN1shBBpj2rQwvVQPU8e9MRwUDizOYHLnuAXQG89Xm7kXL03HIX0uzELcY2Q6Bw68BcGQHq-nyC2U6h0Uv277pg~FIwatR84y1FZiswIVmZwfPmpkvR0X~1skuooCPGiztAXREe-hsg_", + "signatureAlgorithm": "RSA-SHA1" + } + }, + { + "id": "custom-policy-url-with-wildcard", + "documentation": "Sign a URL with wildcard resource pattern", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/images/photo1.jpg", + "resourceUrlPattern": "https://d111111abcdef8.cloudfront.net/images/*", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem", + "expirationDate": 1767290400 + }, + "expected": { + "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/images/*\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}", + "queryParams": { + "Key-Pair-Id": "K1TESTKEY" + }, + "signature": "nP~GtuBVUSEUDMXW1ZTGTx~BUQfjl7FsWBQBKwnTNhHlBAeYlzdbMGMphViPQbjawdYt1Z~iBHDr0ctooQyhEdsXwZPQKmRJQm3yIxmbvnuCzB2qdMDVGRKH8l3PnfAJErrgUVIzb6m3KUdtIDvSkLV~Mb0tyQJwZ6QE4BdlEmQ_", + "signatureAlgorithm": "RSA-SHA1" + } + }, + { + "id": "canned-policy-cookies", + "documentation": "Generate signed cookies with canned policy", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/*", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem", + "expirationDate": 1767290400 + }, + "expected": { + "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/*\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}", + "cookies": { + "CloudFront-Expires": "1767290400", + "CloudFront-Key-Pair-Id": "K1TESTKEY", + "CloudFront-Signature": "e0yIAbSak8fgGDJfX0bwVT0uDrzdr6fV3kzHuZOGYxeitO8H-tvFuce7~aOGFNReoi6LNhYpvr0wP99~2KKbrbetwLmIdUnTToOabNamMDJSGZgVK31XPWmuxq~TGUB-4v338iQhpP5qk8rkUe3meE5VXlchIb4vvnzph83X~Z8_" + }, + "signatureAlgorithm": "RSA-SHA1" + } + }, + { + "id": "custom-policy-cookies", + "documentation": "Generate signed cookies with custom policy", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/*", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem", + "expirationDate": 1767290400, + "ipRange": "10.0.0.0/8" + }, + "expected": { + "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/*\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400},\"IpAddress\":{\"AWS:SourceIp\":\"10.0.0.0/8\"}}}]}", + "cookies": { + "CloudFront-Key-Pair-Id": "K1TESTKEY", + "CloudFront-Policy": "eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9kMTExMTExYWJjZGVmOC5jbG91ZGZyb250Lm5ldC8qIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzY3MjkwNDAwfSwiSXBBZGRyZXNzIjp7IkFXUzpTb3VyY2VJcCI6IjEwLjAuMC4wLzgifX19XX0_", + "CloudFront-Signature": "r28Jnd0t9aq7cu0k9jGWl4L0YsRxgueZtGRw5oEEspU9-eIPGM~ZGMQh36~5HpKC5c67cZjDgJcsqrCacmTHMZZx613gbeYAsx2-hEatU8URiuNHnVp4hPV3HqtbuZ6Din9iEZUpOBYVg6DWGEFJRCQ7SPouBhhJdYDZZOPHGpA_" + }, + "signatureAlgorithm": "RSA-SHA1" + } + }, + { + "id": "ecdsa-canned-policy-url", + "documentation": "Sign a URL with ECDSA key and canned policy", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/video.mp4", + "keyPairId": "K2ECDSATEST", + "privateKeyFile": "test-ecdsa-key.pem", + "expirationDate": 1767290400 + }, + "expected": { + "policyJson": "{\"Statement\":[{\"Resource\":\"https://d111111abcdef8.cloudfront.net/video.mp4\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":1767290400}}}]}", + "queryParams": { + "Expires": "1767290400", + "Key-Pair-Id": "K2ECDSATEST" + }, + "signature": "MEQCIDS3JTjLLIRne5G3fDjf6MwgCmckmYVlJhqGVMl0Q4reAiBjXw1FMf9j03wqAHeE4LfRPjOVkp-jWhmDfHVwd7kkpA__", + "signatureAlgorithm": "ECDSA-SHA1" + } + }, + { + "id": "error-missing-expiration", + "documentation": "Error when expiration date is not provided", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/file.txt", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem" + }, + "expected": { + "error": true, + "errorContains": ["expiration", "required"] + } + }, + { + "id": "error-active-after-expiration", + "documentation": "Error when activeDate is after expirationDate", + "input": { + "resourceUrl": "https://d111111abcdef8.cloudfront.net/file.txt", + "keyPairId": "K1TESTKEY", + "privateKeyFile": "test-rsa-key.pem", + "expirationDate": 1767200000, + "activeDate": 1767290400 + }, + "expected": { + "error": true, + "errorContains": ["active", "before", "expiration"] + } + } +] diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-ecdsa-key.pem b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-ecdsa-key.pem new file mode 100644 index 00000000000..4b13d33be7e --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-ecdsa-key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrYlqAmw042lSYigS +/hH1a8NamwSmZHOnIfkoDPMjHbKhRANCAAS70BK5GOWO0HlQcodlwYOKG45Ng7cd +agIYyRgZUEId3gsFY43KtQPcgKLUQ1NPOJyuFcYCtki460UsOV1ThTxP +-----END PRIVATE KEY----- diff --git a/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-rsa-key.pem b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-rsa-key.pem new file mode 100644 index 00000000000..c795cc92a3b --- /dev/null +++ b/aws/rust-runtime/aws-sdk-cloudfront-signer/tests/test-rsa-key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAL1XcBtoCSzdyqqm +CFfh0XeLiuCtlE6bXUbscAbl7AUISzbQNKzgO1FwRBDMddfYyOQ/2nDPzLhgAK2E +Kv+dEi4/YEdLg2cbOaxjfhSn6ycwM+C502Ls/CtxHICcqs4cQs7BTlFfk32gy7Oq +kLTbTttIGMWasBAQHLhbJZstDp2vAgMBAAECgYAoujMqIgG/PeIHPPmUdcWJ0mFI +HO5GzlKNG/So9zICjxsmqjh6ay03Qk/R0TkT+dSUjEufcoNVsYjTyhc5rn6nR0SK +UL2IG5r6yFzlh+27u0/CzR1x61zWj01FwI8tacyR1tskyt7jqvT6eYFb1in2BDlE +qdNgTxVrjVBlxU6hkQJBAON8RGwO3g5Pxt0k0yQ6uSu3xj1/2uq4njKcoDgyFcNp +M7ZFW8SX0ZZw3wqSPlzs7mRI79b+G+M97nBw78kdKBkCQQDVEy4WnA3VRSlBRjVH +HwLyK7B9FaRJi70cb0JePnazEZS74FeT6BFW0HsNNeq+awCEcvemKpsOa8nr8QgM +K00HAkEA2keyS9GURz1Lf4VHSHtElOt5QCe/wvw1aDEcF/APK/t1UE+LN7/Jr0ZM +7pLXXklGklneMXiQ/+K8OY5Ut7DPeQJAATjy8r5Cdg7HhdBZTecnpSwK/yy4nJNo +qlkZEGFbXPuk1s8asYaLUuwvSIwepKkIf7oJIbLs4NBNgEUJvsgg0QJBAJx2KEC9 +KMs82U4qjLE5vA87r4s4llGgvR+QsYfiwr8LUMlN47DLGrjNpb3JZ58j9Cw3zFPh +yLL0lGtqf49VRDs= +-----END PRIVATE KEY----- diff --git a/buildSrc/src/main/kotlin/CrateSet.kt b/buildSrc/src/main/kotlin/CrateSet.kt index a5ba336067e..9124c6befc5 100644 --- a/buildSrc/src/main/kotlin/CrateSet.kt +++ b/buildSrc/src/main/kotlin/CrateSet.kt @@ -50,6 +50,7 @@ object CrateSet { "aws-runtime-api", "aws-sigv4", "aws-types", + "aws-sdk-cloudfront-url-signer", ).map { Crate(it, version(it)) } val SMITHY_RUNTIME_COMMON = diff --git a/tools/ci-build/publisher/src/package.rs b/tools/ci-build/publisher/src/package.rs index 01f764057e7..d0b49e70dd7 100644 --- a/tools/ci-build/publisher/src/package.rs +++ b/tools/ci-build/publisher/src/package.rs @@ -28,11 +28,16 @@ pub struct PackageStats { pub aws_runtime_crates: usize, /// Number of AWS service crates pub aws_sdk_crates: usize, + /// Number of high level library crates + pub aws_sdk_hll_crates: usize, } impl PackageStats { pub fn total(&self) -> usize { - self.smithy_runtime_crates + self.aws_runtime_crates + self.aws_sdk_crates + self.smithy_runtime_crates + + self.aws_runtime_crates + + self.aws_sdk_crates + + self.aws_sdk_hll_crates } fn calculate(batches: &[PackageBatch]) -> PackageStats { @@ -43,6 +48,7 @@ impl PackageStats { PackageCategory::SmithyRuntime => stats.smithy_runtime_crates += 1, PackageCategory::AwsRuntime => stats.aws_runtime_crates += 1, PackageCategory::AwsSdk => stats.aws_sdk_crates += 1, + PackageCategory::AwsSdkHll => stats.aws_sdk_hll_crates += 1, PackageCategory::Unknown => { warn!("Unrecognized crate: {}", package.handle.name); } diff --git a/tools/ci-build/sdk-lints/src/readmes.rs b/tools/ci-build/sdk-lints/src/readmes.rs index a6fa0b9604e..96f6ad82057 100644 --- a/tools/ci-build/sdk-lints/src/readmes.rs +++ b/tools/ci-build/sdk-lints/src/readmes.rs @@ -14,6 +14,7 @@ const CRATES_TO_BE_USED_DIRECTLY: &[&str] = [ "aws-smithy-mocks", "aws-smithy-mocks-experimental", "aws-smithy-experimental", + "aws-sdk-cloudfront-signer", ] .as_slice(); diff --git a/tools/ci-build/smithy-rs-tool-common/src/package.rs b/tools/ci-build/smithy-rs-tool-common/src/package.rs index f5851e94bc3..c7caf095722 100644 --- a/tools/ci-build/smithy-rs-tool-common/src/package.rs +++ b/tools/ci-build/smithy-rs-tool-common/src/package.rs @@ -17,23 +17,32 @@ use std::{ pub const SMITHY_PREFIX: &str = "aws-smithy-"; pub const SDK_PREFIX: &str = "aws-sdk-"; +// AWS SDK High Level Libraries +pub(crate) static AWS_SDK_HLL_PACKAGES: &[&str] = &["aws-sdk-cloudfront-signer"]; + #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)] pub enum PackageCategory { SmithyRuntime, AwsRuntime, AwsSdk, + AwsSdkHll, Unknown, } impl PackageCategory { /// Returns true if the category is `AwsRuntime` or `AwsSdk` pub fn is_sdk(&self) -> bool { - matches!(self, PackageCategory::AwsRuntime | PackageCategory::AwsSdk) + matches!( + self, + PackageCategory::AwsRuntime | PackageCategory::AwsSdk | PackageCategory::AwsSdkHll + ) } /// Categorizes a package based on its name pub fn from_package_name(name: &str) -> PackageCategory { - if name.starts_with(SMITHY_PREFIX) { + if AWS_SDK_HLL_PACKAGES.contains(&name) { + PackageCategory::AwsSdkHll + } else if name.starts_with(SMITHY_PREFIX) { PackageCategory::SmithyRuntime } else if name.starts_with(SDK_PREFIX) { PackageCategory::AwsSdk