From db12cdddc233372e19403886cd2d54543f71378c Mon Sep 17 00:00:00 2001 From: miota Date: Fri, 23 Jan 2026 00:06:46 +0900 Subject: [PATCH 1/7] chore: update dependencies and add jsonwebtoken --- Cargo.lock | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + 2 files changed, 185 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d17c491..7dfd94d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,7 @@ dependencies = [ "const-hex", "enum_dispatch", "hex-literal", + "jsonwebtoken", "reqwest", "reqwest-middleware", "reqwest-retry", @@ -118,6 +119,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -195,6 +219,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -258,6 +284,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -368,6 +403,15 @@ dependencies = [ "serde", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "deunicode" version = "1.6.2" @@ -415,6 +459,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -499,6 +549,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -998,6 +1054,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -1008,6 +1074,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "10.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" +dependencies = [ + "aws-lc-rs", + "base64", + "getrandom 0.2.17", + "js-sys", + "pem", + "serde", + "serde_json", + "signature", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1098,6 +1181,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-format" version = "0.4.4" @@ -1108,6 +1207,15 @@ dependencies = [ "itoa", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1205,6 +1313,16 @@ dependencies = [ "regex", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1313,6 +1431,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1656,7 +1780,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -1704,7 +1828,7 @@ checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -1825,6 +1949,27 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "time", +] + [[package]] name = "siphasher" version = "1.0.2" @@ -2011,6 +2156,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2251,6 +2427,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index cf9755d..2497b0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ color-eyre = { version = "0.6.5", features = ["track-caller", "capture-spantrace const-hex = "1.17.0" enum_dispatch = "0.3.13" hex-literal = "1.1.0" +jsonwebtoken = { version = "10.2.0", features = ["aws_lc_rs"] } reqwest = { version = "0.12.7", features = ["json", "rustls-tls"], default-features = false } reqwest-middleware = { version = "0.4.2", default-features = false, features = ["json", "rustls-tls"] } reqwest-retry = "0.7.0" From 31d9fee2c6c92929de6ea23b37df1086397fe279 Mon Sep 17 00:00:00 2001 From: miota Date: Thu, 26 Feb 2026 00:48:34 +0900 Subject: [PATCH 2/7] feat: implement functionality for creating JWTs using an Amplify TPK --- Cargo.lock | 434 +++++++++++++++++++++++++++++------ Cargo.toml | 5 +- ecdsa-p521-local.private.pem | 8 + ecdsa-p521-local.public.pem | 6 + src/auth/mod.rs | 1 + src/auth/tpk/mod.rs | 370 +++++++++++++++++++++++++++++ 6 files changed, 758 insertions(+), 66 deletions(-) create mode 100644 ecdsa-p521-local.private.pem create mode 100644 ecdsa-p521-local.public.pem create mode 100644 src/auth/tpk/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7dfd94d..fbef606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,7 @@ dependencies = [ "reqwest-middleware", "reqwest-retry", "serde", + "serde_json", "sha2", "thiserror 1.0.69", "tokei", @@ -119,29 +120,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-lc-rs" -version = "1.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" -dependencies = [ - "aws-lc-sys", - "untrusted 0.7.1", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -157,12 +135,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "1.3.2" @@ -219,8 +209,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -284,15 +272,6 @@ dependencies = [ "vec_map", ] -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - [[package]] name = "color-eyre" version = "0.6.5" @@ -333,6 +312,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -382,6 +367,18 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-bigint" +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]] name = "crypto-common" version = "0.1.7" @@ -392,6 +389,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "4.0.2" @@ -403,6 +427,17 @@ dependencies = [ "serde", ] +[[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" @@ -425,7 +460,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -460,10 +497,42 @@ dependencies = [ ] [[package]] -name = "dunce" -version = "1.0.5" +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] [[package]] name = "either" @@ -471,6 +540,27 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -534,6 +624,22 @@ dependencies = [ "once_cell", ] +[[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 = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.8" @@ -549,12 +655,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.31" @@ -652,6 +752,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -735,6 +836,17 @@ dependencies = [ "memmap2", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -756,6 +868,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.4.0" @@ -1054,16 +1184,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.85" @@ -1076,17 +1196,23 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e" +version = "10.3.0" +source = "git+https://github.com/arsenin-kitsoft/jsonwebtoken?rev=fd96c1c#fd96c1c1897854f12db17a4b7c8407b43ab1f2aa" dependencies = [ - "aws-lc-rs", "base64", + "ed25519-dalek", "getrandom 0.2.17", + "hmac", "js-sys", + "p256", + "p384", + "p521", "pem", + "rand 0.8.5", + "rsa", "serde", "serde_json", + "sha2", "signature", "simple_asn1", ] @@ -1096,6 +1222,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" @@ -1191,6 +1320,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" @@ -1216,6 +1361,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-traits" version = "0.2.19" @@ -1223,6 +1379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1256,6 +1413,44 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1323,6 +1518,15 @@ dependencies = [ "serde_core", ] +[[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" @@ -1422,6 +1626,27 @@ 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", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1446,6 +1671,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1770,6 +2004,16 @@ dependencies = [ "rand 0.8.5", ] +[[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" @@ -1780,10 +2024,30 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -1796,6 +2060,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.23.36" @@ -1828,7 +2101,7 @@ checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -1858,6 +2131,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1955,6 +2248,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core 0.6.4", ] @@ -2008,6 +2302,22 @@ 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.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2427,12 +2737,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 2497b0c..e49a459 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ color-eyre = { version = "0.6.5", features = ["track-caller", "capture-spantrace const-hex = "1.17.0" enum_dispatch = "0.3.13" hex-literal = "1.1.0" -jsonwebtoken = { version = "10.2.0", features = ["aws_lc_rs"] } +jsonwebtoken = { git = "https://github.com/arsenin-kitsoft/jsonwebtoken", rev = "fd96c1c", features = ["use_pem", "rust_crypto"] } reqwest = { version = "0.12.7", features = ["json", "rustls-tls"], default-features = false } reqwest-middleware = { version = "0.4.2", default-features = false, features = ["json", "rustls-tls"] } reqwest-retry = "0.7.0" @@ -24,3 +24,6 @@ sha2 = "0.10.8" thiserror = "1.0.63" tokei = "12.1.2" tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "signal", "process", "sync", "time", "io-util", "io-std", "parking_lot"] } + +[dev-dependencies] +serde_json = "1" diff --git a/ecdsa-p521-local.private.pem b/ecdsa-p521-local.private.pem new file mode 100644 index 0000000..6ebb5ef --- /dev/null +++ b/ecdsa-p521-local.private.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBkVnED1X34y7oiRp4 +CDB4iOts9mlR5Ht24onc1ZyLnWwD6MwTW2Lyt28Kq1lKTHDFSgox+gzxsiHwqpg1 +UDMMsn+hgYkDgYYABAHVd8RNe9CbyN1qxY0SYlxKXLLe+hWhhK03du+1MCpxBYm7 +1Z8A3C+4RejytDJZs2bGlrnCKjDGB2dYgqxj/mN4yQB+Wad94wCElPaqpN8qRnt8 +FtFHmSkYOdBwfSyxYd+jC5c0HaxiVeIC0jONtC5SC75FXh1OhjhjuRTqmX6Qvox8 +3g== +-----END PRIVATE KEY----- diff --git a/ecdsa-p521-local.public.pem b/ecdsa-p521-local.public.pem new file mode 100644 index 0000000..ff0e161 --- /dev/null +++ b/ecdsa-p521-local.public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB1XfETXvQm8jdasWNEmJcSlyy3voV +oYStN3bvtTAqcQWJu9WfANwvuEXo8rQyWbNmxpa5wiowxgdnWIKsY/5jeMkAflmn +feMAhJT2qqTfKkZ7fBbRR5kpGDnQcH0ssWHfowuXNB2sYlXiAtIzjbQuUgu+RV4d +ToY4Y7kU6pl+kL6MfN4= +-----END PUBLIC KEY----- diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 30622d6..095a53e 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod amplify; pub(crate) mod github; pub(crate) mod gitlab; +pub(crate) mod tpk; use color_eyre::eyre::Result; diff --git a/src/auth/tpk/mod.rs b/src/auth/tpk/mod.rs new file mode 100644 index 0000000..8d7446d --- /dev/null +++ b/src/auth/tpk/mod.rs @@ -0,0 +1,370 @@ +//! Platform-independent JWT creation using a user's Trusted Private Key +//! +//! When a platform-native OIDC token (e.g. `AMPLIFY_ID_TOKEN` from GitLab) is +//! not available, the runner can fall back to signing its own JWT, pulling +//! the necessary information from the runner's CI environment to associate a +//! CI pipeline (and artifacts) with a customer account, using the private key +//! from a keypair that the user creates in their Amplify dashboard by setting +//! a `TRUSTED_PRIVATE_KEY` environment variable in their CI system. +//! +//! # Defaults +//! +//! | Setting | Value | +//! |-----------|----------------------------------| +//! | Issuer | `https://tpk.amplify.security` | +//! | Audience | `https://api.amplify.security` | +//! | Algorithm | ES512 (ECDSA P-521 + SHA-512) | +//! | Token TTL | 3 600 seconds (1 hour) | + +use color_eyre::eyre::{Result, WrapErr}; +use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; +use serde::Serialize; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +/// Default `iss` claim for trusted-private-key JWTs. +pub const DEFAULT_ISSUER: &str = "https://tpk.amplify.security"; +/// Default `aud` claim for trusted-private-key JWTs. +pub const DEFAULT_AUDIENCE: &str = "https://api.amplify.security"; +/// Default token lifetime in seconds (1 hour). +pub const DEFAULT_TOKEN_TTL_SECS: u64 = 3_600; + +// ─── signer ────────────────────────────────────────────────────────────────── + +/// JWT signer backed by a customer-provided EC private key. +/// +/// # Example +/// +/// ```rust,ignore +/// use serde::Serialize; +/// +/// #[derive(Serialize)] +/// struct MyClaims { pipeline_id: String } +/// +/// let token = TpkJwt::from_ec_pem(pem_bytes)? +/// .with_issuer("https://ci.example.com") +/// .create_token(MyClaims { pipeline_id: "42".into() }, DEFAULT_TOKEN_TTL_SECS)?; +/// ``` +pub(crate) struct TpkJwt { + /// Value placed in the `iss` claim. + pub issuer: String, + /// Value placed in the `aud` claim. + pub audience: String, + encoding_key: EncodingKey, + algorithm: Algorithm, +} + +impl TpkJwt { + /// Build a signer from a PEM-encoded EC private key. + /// + /// Defaults to [`Algorithm::ES512`] (ECDSA P-521), issuer + /// [`DEFAULT_ISSUER`], and audience [`DEFAULT_AUDIENCE`]. + pub fn from_ec_pem(pem: &[u8]) -> Result { + let encoding_key = EncodingKey::from_ec_pem(pem) + .wrap_err("Failed to parse EC private key PEM for TPK JWT")?; + Ok(Self { + issuer: DEFAULT_ISSUER.to_string(), + audience: DEFAULT_AUDIENCE.to_string(), + encoding_key, + algorithm: Algorithm::ES512, + }) + } + + /// Attempt to build a signer from the `TRUSTED_PRIVATE_KEY` environment + /// variable, which must contain a PEM-encoded EC private key. + /// + /// Returns `Ok(None)` when the variable is not set. + pub fn from_env() -> Result> { + match std::env::var("TRUSTED_PRIVATE_KEY") { + Ok(pem) => Ok(Some(Self::from_ec_pem(pem.as_bytes())?)), + Err(_) => Ok(None), + } + } + + /// Override the `iss` (issuer) claim. Returns `self` for chaining. + pub fn with_issuer(mut self, issuer: impl Into) -> Self { + self.issuer = issuer.into(); + self + } + + /// Override the `aud` (audience) claim. Returns `self` for chaining. + pub fn with_audience(mut self, audience: impl Into) -> Self { + self.audience = audience.into(); + self + } + + /// Override the signing algorithm (default: [`Algorithm::ES512`]). + /// Returns `self` for chaining. + #[allow(dead_code)] + pub fn with_algorithm(mut self, algorithm: Algorithm) -> Self { + self.algorithm = algorithm; + self + } + + /// Sign and return a JWT containing the standard claims (`iss`, `aud`, + /// `iat`, `nbf`, `exp`) merged with the caller-supplied `custom_claims`. + /// + /// `ttl_secs` controls how long the token is valid for. + pub fn create_token(&self, custom_claims: C, ttl_secs: u64) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() as usize; + + let claims = TpkClaims { + iss: self.issuer.clone(), + aud: self.audience.clone(), + iat: now, + nbf: now, + exp: now + ttl_secs as usize, + extra: custom_claims, + }; + + let header = Header::new(self.algorithm); + encode(&header, &claims, &self.encoding_key).wrap_err("Failed to encode TPK JWT") + } +} + +// ─── internal claims envelope ──────────────────────────────────────────────── + +/// Standard JWT fields with a generic extra payload flattened alongside them. +#[derive(Debug, Serialize)] +struct TpkClaims { + iss: String, + aud: String, + iat: usize, + nbf: usize, + exp: usize, + #[serde(flatten)] + extra: C, +} + +// ─── tests ─────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use jsonwebtoken::{decode, DecodingKey, Validation}; + use serde::{Deserialize, Serialize}; + use std::sync::Mutex; + + // Serialise all environment-variable mutations to prevent data races + // between tests that run in parallel within the same process. + static ENV_MUTEX: Mutex<()> = Mutex::new(()); + + // Paths are relative to this source file: + // runner/src/auth/tpk/mod.rs → runner/ + const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../../ecdsa-p521-local.private.pem"); + const TEST_PUBLIC_KEY_PEM: &str = include_str!("../../../ecdsa-p521-local.public.pem"); + + // ── helpers ─────────────────────────────────────────────────────────── + + fn make_signer() -> TpkJwt { + TpkJwt::from_ec_pem(TEST_PRIVATE_KEY_PEM.as_bytes()) + .expect("test private key should be valid") + } + + fn make_decoding_key() -> DecodingKey { + DecodingKey::from_ec_pem(TEST_PUBLIC_KEY_PEM.as_bytes()) + .expect("test public key should be valid") + } + + fn make_validation(audience: &str) -> Validation { + let mut v = Validation::new(Algorithm::ES512); + v.set_audience(&[audience]); + v + } + + #[derive(Debug, Serialize, Deserialize)] + struct SampleClaims { + sub: String, + custom_field: String, + } + + // ── construction ────────────────────────────────────────────────────── + + #[test] + fn test_defaults_are_set() { + let signer = make_signer(); + assert_eq!(signer.issuer, DEFAULT_ISSUER); + assert_eq!(signer.audience, DEFAULT_AUDIENCE); + } + + #[test] + fn test_builder_overrides_issuer_and_audience() { + let signer = make_signer() + .with_issuer("https://ci.issuer.example.com") + .with_audience("https://ci.audience.example.com"); + assert_eq!(signer.issuer, "https://ci.issuer.example.com"); + assert_eq!(signer.audience, "https://ci.audience.example.com"); + } + + // ── token shape ─────────────────────────────────────────────────────── + + #[test] + fn test_create_token_produces_three_part_jwt() { + let token = make_signer() + .create_token( + SampleClaims { + sub: "test-subject".into(), + custom_field: "hello".into(), + }, + 3600, + ) + .expect("token creation should succeed"); + + assert!(!token.is_empty()); + // A compact-serialised JWT has exactly two '.' separators. + assert_eq!( + token.chars().filter(|&c| c == '.').count(), + 2, + "token should be a three-part compact JWT" + ); + } + + // ── round-trip verification ─────────────────────────────────────────── + + #[test] + fn test_token_verifies_with_matching_public_key() { + let token = make_signer() + .create_token( + SampleClaims { + sub: "verified-subject".into(), + custom_field: "check-me".into(), + }, + 3600, + ) + .unwrap(); + + let decoded = decode::( + &token, + &make_decoding_key(), + &make_validation(DEFAULT_AUDIENCE), + ) + .expect("token should verify with the matching public key"); + + let claims = decoded.claims; + assert_eq!(claims["iss"], DEFAULT_ISSUER); + assert_eq!(claims["aud"], DEFAULT_AUDIENCE); + assert_eq!(claims["sub"], "verified-subject"); + assert_eq!(claims["custom_field"], "check-me"); + } + + #[test] + fn test_custom_issuer_and_audience_appear_in_decoded_token() { + let signer = make_signer() + .with_issuer("https://my.issuer.test") + .with_audience("https://my.audience.test"); + + let token = signer + .create_token( + SampleClaims { + sub: "s".into(), + custom_field: "v".into(), + }, + 3600, + ) + .unwrap(); + + let decoded = decode::( + &token, + &make_decoding_key(), + &make_validation("https://my.audience.test"), + ) + .unwrap(); + + assert_eq!(decoded.claims["iss"], "https://my.issuer.test"); + assert_eq!(decoded.claims["aud"], "https://my.audience.test"); + } + + #[test] + fn test_standard_time_claims_are_present_and_consistent() { + let ttl: u64 = 1_800; + let token = make_signer() + .create_token( + SampleClaims { + sub: "t".into(), + custom_field: "v".into(), + }, + ttl, + ) + .unwrap(); + + let decoded = decode::( + &token, + &make_decoding_key(), + &make_validation(DEFAULT_AUDIENCE), + ) + .unwrap(); + let claims = decoded.claims; + + let iat = claims["iat"].as_u64().expect("iat should be a number"); + let exp = claims["exp"].as_u64().expect("exp should be a number"); + let nbf = claims["nbf"].as_u64().expect("nbf should be a number"); + + assert_eq!(exp - iat, ttl, "exp should equal iat + ttl"); + assert_eq!(iat, nbf, "iat and nbf should be equal at issuance"); + } + + #[test] + fn test_wrong_public_key_fails_verification() { + // Forge a second signer from the same key; in practice this tests + // that the validation machinery rejects a structurally valid but + // wrongly-signed token when the key pair does not match. + let token = make_signer() + .create_token( + SampleClaims { + sub: "x".into(), + custom_field: "y".into(), + }, + 3600, + ) + .unwrap(); + + // Corrupt the signature segment (last third of the JWT). + let mut parts: Vec<&str> = token.splitn(3, '.').collect(); + let mut bad_sig = parts[2].to_string(); + // Flip a character to invalidate the signature. + if bad_sig.starts_with('A') { + bad_sig.replace_range(0..1, "B"); + } else { + bad_sig.replace_range(0..1, "A"); + } + parts[2] = &bad_sig; + let tampered = parts.join("."); + + let result = decode::( + &tampered, + &make_decoding_key(), + &make_validation(DEFAULT_AUDIENCE), + ); + assert!(result.is_err(), "tampered token should fail verification"); + } + + // ── from_env ────────────────────────────────────────────────────────── + + #[test] + fn test_from_env_returns_none_when_var_absent() { + let _lock = ENV_MUTEX.lock().unwrap(); + std::env::remove_var("TRUSTED_PRIVATE_KEY"); + let result = TpkJwt::from_env().unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_from_env_returns_signer_when_var_is_valid_key() { + let _lock = ENV_MUTEX.lock().unwrap(); + std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); + let result = TpkJwt::from_env().unwrap(); + assert!(result.is_some()); + std::env::remove_var("TRUSTED_PRIVATE_KEY"); + } + + #[test] + fn test_from_env_errors_on_invalid_pem() { + let _lock = ENV_MUTEX.lock().unwrap(); + std::env::set_var("TRUSTED_PRIVATE_KEY", "not-a-valid-pem"); + let result = TpkJwt::from_env(); + assert!(result.is_err(), "an invalid PEM should produce an error"); + std::env::remove_var("TRUSTED_PRIVATE_KEY"); + } +} From 9982de191f110f01945435f429f0d2165e529c77 Mon Sep 17 00:00:00 2001 From: miota Date: Thu, 26 Feb 2026 01:03:49 +0900 Subject: [PATCH 3/7] feat: add TPK-based JWT generation for Gitlab environments --- src/auth/gitlab/mod.rs | 274 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 266 insertions(+), 8 deletions(-) diff --git a/src/auth/gitlab/mod.rs b/src/auth/gitlab/mod.rs index 12591e5..f0c058f 100644 --- a/src/auth/gitlab/mod.rs +++ b/src/auth/gitlab/mod.rs @@ -1,6 +1,21 @@ -//! Authentication for Gitlab Pipelines +//! Authentication for GitLab CI pipelines. +//! +//! Token resolution order: +//! +//! 1. **`AMPLIFY_ID_TOKEN`** – an OIDC ID token issued by GitLab itself. +//! This token is automatically configured in Amplify's runner component +//! and should be preferred when in use. +//! 2. **`TRUSTED_PRIVATE_KEY`** – a PEM-encoded private key supplied by the +//! user that is configured in Amplify. The runner signs its own JWT and +//! includes a set of GitLab predefined CI/CD variables as claims so that +//! the Amplify API can identify the pipeline and project. -use color_eyre::eyre::{eyre, Result}; +use color_eyre::eyre::{Result, WrapErr}; +use serde::{Deserialize, Serialize}; + +use crate::auth::tpk::{TpkJwt, DEFAULT_TOKEN_TTL_SECS}; + +// ─── auth provider ─────────────────────────────────────────────────────────── #[derive(Debug, Clone)] pub(crate) struct GitlabAuth { @@ -12,15 +27,258 @@ impl GitlabAuth { Ok(GitlabAuth { jwt: None }) } + /// Return a bearer token that identifies this pipeline run to the Amplify API. pub async fn get_token(&mut self) -> Result { - // GitLab generates JWTs for specific audiences within the pipeline - // configuration itself, so all we have to do is pull the env variable - // https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html if let Ok(token) = std::env::var("AMPLIFY_ID_TOKEN") { - self.jwt = Some(token); - return Ok(self.jwt.clone().unwrap()); + self.jwt = Some(token.clone()); + return Ok(token); + } + + if let Some(signer) = + TpkJwt::from_env().wrap_err("Failed to load TRUSTED_PRIVATE_KEY for GitLab TPK JWT")? + { + let claims = GitlabTpkClaims::from_env() + .wrap_err("Failed to read required GitLab CI variables for TPK JWT")?; + let token = signer + .create_token(claims, DEFAULT_TOKEN_TTL_SECS) + .wrap_err("Failed to sign GitLab TPK JWT")?; + self.jwt = Some(token.clone()); + return Ok(token); } - Err(eyre!("Failed to locate an ID Token from Gitlab.")) + Err(color_eyre::eyre::eyre!( + "No GitLab ID token found. \ + Either use the amplify-security/components/runner component in \ + your `.gitlab-ci.yml`, or create a keypair in Amplify and \ + configure `TRUSTED_PRIVATE_KEY` to the private key in your CI \ + environment variables." + )) + } +} + +/// JWT payload for the Trusted Public Key fallback path. +/// +/// The field names match the claim names used in real GitLab OIDC ID tokens so +/// that the Amplify API can handle both token kinds uniformly. +#[derive(Debug, Serialize, Deserialize)] +struct GitlabTpkClaims { + /// URL of the GitLab instance (`CI_SERVER_URL`). + /// e.g. `"https://gitlab.com"` or `"https://gitlab.example.com:8080"`. + ci_server_url: String, + + /// Instance-level pipeline ID (`CI_PIPELINE_ID`). + pipeline_id: String, + + /// Instance-level project ID (`CI_PROJECT_ID`). + project_id: String, + + /// Namespace + project path (`CI_PROJECT_PATH`), e.g. `"my-group/my-project"`. + project_path: String, + + /// The branch or tag name (`CI_COMMIT_REF_NAME`). + /// Renamed to `ref` to match the GitLab OIDC token schema. + #[serde(rename = "ref")] + git_ref: String, + + /// Instance-level job ID (`CI_JOB_ID`). + job_id: String, + + /// Full commit SHA (`CI_COMMIT_SHA`). + sha: String, +} + +impl GitlabTpkClaims { + /// Populate claims from the current process environment. + /// + /// All fields are required. Every variable listed here is a predefined + /// GitLab CI/CD variable that is always present in any GitLab CI job. + fn from_env() -> Result { + Ok(Self { + ci_server_url: std::env::var("CI_SERVER_URL") + .wrap_err("Expected CI_SERVER_URL to be set, but it wasn't!")?, + pipeline_id: std::env::var("CI_PIPELINE_ID") + .wrap_err("Expected CI_PIPELINE_ID to be set, but it wasn't!")?, + project_id: std::env::var("CI_PROJECT_ID") + .wrap_err("Expected CI_PROJECT_ID to be set, but it wasn't!")?, + project_path: std::env::var("CI_PROJECT_PATH") + .wrap_err("Expected CI_PROJECT_PATH to be set, but it wasn't!")?, + git_ref: std::env::var("CI_COMMIT_REF_NAME") + .wrap_err("Expected CI_COMMIT_REF_NAME to be set, but it wasn't!")?, + job_id: std::env::var("CI_JOB_ID") + .wrap_err("Expected CI_JOB_ID to be set, but it wasn't!")?, + sha: std::env::var("CI_COMMIT_SHA") + .wrap_err("Expected CI_COMMIT_SHA to be set, but it wasn't!")?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; + use std::sync::Mutex; + + // Serialise all environment-variable mutations to prevent data races + // between tests that run in parallel within the same process. + static ENV_MUTEX: Mutex<()> = Mutex::new(()); + + const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../../ecdsa-p521-local.private.pem"); + const TEST_PUBLIC_KEY_PEM: &str = include_str!("../../../ecdsa-p521-local.public.pem"); + + fn set_all_gitlab_vars() { + std::env::set_var("CI_SERVER_URL", "https://gitlab.example.com"); + std::env::set_var("CI_PIPELINE_ID", "1001"); + std::env::set_var("CI_PROJECT_ID", "42"); + std::env::set_var("CI_PROJECT_PATH", "my-group/my-project"); + std::env::set_var("CI_COMMIT_REF_NAME", "main"); + std::env::set_var("CI_JOB_ID", "9999"); + std::env::set_var("CI_COMMIT_SHA", "abc123def456"); + } + + fn clear_all_vars() { + std::env::remove_var("AMPLIFY_ID_TOKEN"); + std::env::remove_var("TRUSTED_PRIVATE_KEY"); + std::env::remove_var("CI_SERVER_URL"); + std::env::remove_var("CI_PIPELINE_ID"); + std::env::remove_var("CI_PROJECT_ID"); + std::env::remove_var("CI_PROJECT_PATH"); + std::env::remove_var("CI_COMMIT_REF_NAME"); + std::env::remove_var("CI_JOB_ID"); + std::env::remove_var("CI_COMMIT_SHA"); + } + + fn make_validation() -> Validation { + let mut v = Validation::new(Algorithm::ES512); + v.set_audience(&[crate::auth::tpk::DEFAULT_AUDIENCE]); + v + } + + // AMPLIFY_ID_TOKEN usage (from runner component) + + #[tokio::test] + async fn test_amplify_id_token_is_returned_directly() { + let _lock = ENV_MUTEX.lock().unwrap(); + clear_all_vars(); + std::env::set_var("AMPLIFY_ID_TOKEN", "gitlab.issued.token"); + + let mut auth = GitlabAuth::new().unwrap(); + let token = auth.get_token().await.unwrap(); + + assert_eq!(token, "gitlab.issued.token"); + } + + #[tokio::test] + async fn test_amplify_id_token_is_cached_on_auth_struct() { + let _lock = ENV_MUTEX.lock().unwrap(); + clear_all_vars(); + std::env::set_var("AMPLIFY_ID_TOKEN", "cached.token"); + + let mut auth = GitlabAuth::new().unwrap(); + auth.get_token().await.unwrap(); + + assert_eq!(auth.jwt.as_deref(), Some("cached.token")); + } + + // TPK Fallback + + #[tokio::test] + async fn test_tpk_fallback_produces_valid_jwt() { + let _lock = ENV_MUTEX.lock().unwrap(); + clear_all_vars(); + set_all_gitlab_vars(); + std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); + + let mut auth = GitlabAuth::new().unwrap(); + let token = auth.get_token().await.unwrap(); + + let decoding_key = DecodingKey::from_ec_pem(TEST_PUBLIC_KEY_PEM.as_bytes()).unwrap(); + let decoded = + decode::(&token, &decoding_key, &make_validation()).unwrap(); + let claims = decoded.claims; + + assert_eq!(claims["ci_server_url"], "https://gitlab.example.com"); + assert_eq!(claims["pipeline_id"], "1001"); + assert_eq!(claims["project_id"], "42"); + assert_eq!(claims["project_path"], "my-group/my-project"); + assert_eq!(claims["ref"], "main"); + assert_eq!(claims["job_id"], "9999"); + assert_eq!(claims["sha"], "abc123def456"); + } + + #[tokio::test] + async fn test_tpk_fallback_uses_correct_issuer_and_audience() { + let _lock = ENV_MUTEX.lock().unwrap(); + clear_all_vars(); + set_all_gitlab_vars(); + std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); + + let mut auth = GitlabAuth::new().unwrap(); + let token = auth.get_token().await.unwrap(); + + let decoding_key = DecodingKey::from_ec_pem(TEST_PUBLIC_KEY_PEM.as_bytes()).unwrap(); + let decoded = + decode::(&token, &decoding_key, &make_validation()).unwrap(); + let claims = decoded.claims; + + assert_eq!(claims["iss"], crate::auth::tpk::DEFAULT_ISSUER); + assert_eq!(claims["aud"], crate::auth::tpk::DEFAULT_AUDIENCE); + } + + #[tokio::test] + async fn test_tpk_fallback_token_is_cached_on_auth_struct() { + let _lock = ENV_MUTEX.lock().unwrap(); + clear_all_vars(); + set_all_gitlab_vars(); + std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); + + let mut auth = GitlabAuth::new().unwrap(); + let token = auth.get_token().await.unwrap(); + + assert_eq!(auth.jwt.as_deref(), Some(token.as_str())); + } + + // Error Cases + + #[tokio::test] + async fn test_error_when_neither_token_source_is_available() { + let _lock = ENV_MUTEX.lock().unwrap(); + clear_all_vars(); + + let mut auth = GitlabAuth::new().unwrap(); + let result = auth.get_token().await; + + assert!( + result.is_err(), + "should fail when no token source is configured" + ); + } + + #[tokio::test] + async fn test_error_when_tpk_present_but_ci_var_missing() { + let _lock = ENV_MUTEX.lock().unwrap(); + + // Each predefined GitLab CI variable is required; verify that omitting + // any one of them produces an error. + let all_vars = [ + "CI_SERVER_URL", + "CI_PIPELINE_ID", + "CI_PROJECT_ID", + "CI_PROJECT_PATH", + "CI_COMMIT_REF_NAME", + "CI_JOB_ID", + "CI_COMMIT_SHA", + ]; + + for omit in all_vars { + clear_all_vars(); + set_all_gitlab_vars(); + std::env::remove_var(omit); + std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); + + let mut auth = GitlabAuth::new().unwrap(); + let result = auth.get_token().await; + + assert!(result.is_err(), "should fail when {omit} is missing"); + } } } From 371a7f584a03c843fb51defe1c526e1bba92c151 Mon Sep 17 00:00:00 2001 From: miota Date: Thu, 26 Feb 2026 02:06:54 +0900 Subject: [PATCH 4/7] fix: resolve clippy errors, use crate-wide async mutex for env vars during testing --- src/auth/gitlab/mod.rs | 19 +++++++------------ src/auth/tpk/mod.rs | 32 ++++++++++++++++---------------- src/common.rs | 7 +++++++ 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/auth/gitlab/mod.rs b/src/auth/gitlab/mod.rs index f0c058f..6adaa72 100644 --- a/src/auth/gitlab/mod.rs +++ b/src/auth/gitlab/mod.rs @@ -116,11 +116,6 @@ impl GitlabTpkClaims { mod tests { use super::*; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; - use std::sync::Mutex; - - // Serialise all environment-variable mutations to prevent data races - // between tests that run in parallel within the same process. - static ENV_MUTEX: Mutex<()> = Mutex::new(()); const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../../ecdsa-p521-local.private.pem"); const TEST_PUBLIC_KEY_PEM: &str = include_str!("../../../ecdsa-p521-local.public.pem"); @@ -157,7 +152,7 @@ mod tests { #[tokio::test] async fn test_amplify_id_token_is_returned_directly() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; clear_all_vars(); std::env::set_var("AMPLIFY_ID_TOKEN", "gitlab.issued.token"); @@ -169,7 +164,7 @@ mod tests { #[tokio::test] async fn test_amplify_id_token_is_cached_on_auth_struct() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; clear_all_vars(); std::env::set_var("AMPLIFY_ID_TOKEN", "cached.token"); @@ -183,7 +178,7 @@ mod tests { #[tokio::test] async fn test_tpk_fallback_produces_valid_jwt() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; clear_all_vars(); set_all_gitlab_vars(); std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); @@ -207,7 +202,7 @@ mod tests { #[tokio::test] async fn test_tpk_fallback_uses_correct_issuer_and_audience() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; clear_all_vars(); set_all_gitlab_vars(); std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); @@ -226,7 +221,7 @@ mod tests { #[tokio::test] async fn test_tpk_fallback_token_is_cached_on_auth_struct() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; clear_all_vars(); set_all_gitlab_vars(); std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); @@ -241,7 +236,7 @@ mod tests { #[tokio::test] async fn test_error_when_neither_token_source_is_available() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; clear_all_vars(); let mut auth = GitlabAuth::new().unwrap(); @@ -255,7 +250,7 @@ mod tests { #[tokio::test] async fn test_error_when_tpk_present_but_ci_var_missing() { - let _lock = ENV_MUTEX.lock().unwrap(); + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; // Each predefined GitLab CI variable is required; verify that omitting // any one of them produces an error. diff --git a/src/auth/tpk/mod.rs b/src/auth/tpk/mod.rs index 8d7446d..a325204 100644 --- a/src/auth/tpk/mod.rs +++ b/src/auth/tpk/mod.rs @@ -81,12 +81,14 @@ impl TpkJwt { } /// Override the `iss` (issuer) claim. Returns `self` for chaining. + #[allow(dead_code)] pub fn with_issuer(mut self, issuer: impl Into) -> Self { self.issuer = issuer.into(); self } /// Override the `aud` (audience) claim. Returns `self` for chaining. + #[allow(dead_code)] pub fn with_audience(mut self, audience: impl Into) -> Self { self.audience = audience.into(); self @@ -145,12 +147,6 @@ mod tests { use super::*; use jsonwebtoken::{decode, DecodingKey, Validation}; use serde::{Deserialize, Serialize}; - use std::sync::Mutex; - - // Serialise all environment-variable mutations to prevent data races - // between tests that run in parallel within the same process. - static ENV_MUTEX: Mutex<()> = Mutex::new(()); - // Paths are relative to this source file: // runner/src/auth/tpk/mod.rs → runner/ const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../../ecdsa-p521-local.private.pem"); @@ -341,27 +337,31 @@ mod tests { } // ── from_env ────────────────────────────────────────────────────────── - - #[test] - fn test_from_env_returns_none_when_var_absent() { - let _lock = ENV_MUTEX.lock().unwrap(); + // + // These tests are async so they can share the crate-wide ENV_MUTEX + // (tokio::sync::Mutex) with the gitlab tests, preventing races when both + // suites run in parallel and manipulate TRUSTED_PRIVATE_KEY. + + #[tokio::test] + async fn test_from_env_returns_none_when_var_absent() { + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; std::env::remove_var("TRUSTED_PRIVATE_KEY"); let result = TpkJwt::from_env().unwrap(); assert!(result.is_none()); } - #[test] - fn test_from_env_returns_signer_when_var_is_valid_key() { - let _lock = ENV_MUTEX.lock().unwrap(); + #[tokio::test] + async fn test_from_env_returns_signer_when_var_is_valid_key() { + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; std::env::set_var("TRUSTED_PRIVATE_KEY", TEST_PRIVATE_KEY_PEM); let result = TpkJwt::from_env().unwrap(); assert!(result.is_some()); std::env::remove_var("TRUSTED_PRIVATE_KEY"); } - #[test] - fn test_from_env_errors_on_invalid_pem() { - let _lock = ENV_MUTEX.lock().unwrap(); + #[tokio::test] + async fn test_from_env_errors_on_invalid_pem() { + let _lock = crate::common::test_support::ENV_MUTEX.lock().await; std::env::set_var("TRUSTED_PRIVATE_KEY", "not-a-valid-pem"); let result = TpkJwt::from_env(); assert!(result.is_err(), "an invalid PEM should produce an error"); diff --git a/src/common.rs b/src/common.rs index aa4ad9c..75d425d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -27,3 +27,10 @@ impl RetryableStrategy for DefaultRetryStrategyWith401 { } } } + +/// Shared utilities for tests across the crate. +#[cfg(test)] +pub(crate) mod test_support { + /// Async mutex used by every test that reads or writes env vars. + pub(crate) static ENV_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); +} From f89b240ba1426fd2a234b78feb06c406fb1cdaa4 Mon Sep 17 00:00:00 2001 From: miota Date: Thu, 26 Feb 2026 02:08:42 +0900 Subject: [PATCH 5/7] fix: update time to resolve RUSTSEC-2026-0009 --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbef606..19d96ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,9 +1338,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-format" @@ -2468,9 +2468,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -2483,15 +2483,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", From d0b2e795b7e6cb6adadd2ae296bfa1d93dc8e8db Mon Sep 17 00:00:00 2001 From: miota Date: Thu, 26 Feb 2026 02:16:19 +0900 Subject: [PATCH 6/7] fix: add an audit.toml file to ignore RUSTSEC-2023-0071 on unused child dependency (rsa) --- .cargo/audit.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .cargo/audit.toml diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 0000000..5662bb4 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,3 @@ +[advisories] +ignore = ["RUSTSEC-2023-0071"] +# RUSTSEC-2023-0071: rust_crypto pulls in rsa as a dependency, but we don't use those code paths. From ecfd6988c59d0c1734d8ee66d743ee33aa1e8ee1 Mon Sep 17 00:00:00 2001 From: miota Date: Thu, 26 Feb 2026 02:22:46 +0900 Subject: [PATCH 7/7] fix: ignore test private key in Amplify scans --- .amplifyignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .amplifyignore diff --git a/.amplifyignore b/.amplifyignore new file mode 100644 index 0000000..ca24bed --- /dev/null +++ b/.amplifyignore @@ -0,0 +1 @@ +ecdsa-p521-local.private.pem