From 064a2eb293b5b195c4042fb629f480add07b082e Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 13 Feb 2026 17:54:16 -0800 Subject: [PATCH] basic oauth & oidc support this implements login, logout, claim-to-header proxying, and header stripping. the whole domain is protected for a given domain -- there's no partial coverage yet --- Cargo.lock | 2302 +++++++++++++++++++++++++++++++++++++-- Cargo.toml | 16 + flake.lock | 12 +- src/config/format.proto | 27 +- src/cookies.rs | 85 ++ src/gateway.rs | 669 ++++++++++++ src/gateway/oidc.rs | 193 ++++ src/httputil.rs | 118 ++ src/main.rs | 212 +--- src/oauth.rs | 596 ++++++++++ 10 files changed, 3921 insertions(+), 309 deletions(-) create mode 100644 src/cookies.rs create mode 100644 src/gateway.rs create mode 100644 src/gateway/oidc.rs create mode 100644 src/httputil.rs create mode 100644 src/oauth.rs diff --git a/Cargo.lock b/Cargo.lock index 9c195f5..03bd9f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,50 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" +dependencies = [ + "aes", +] + [[package]] name = "ahash" version = "0.8.12" @@ -131,6 +175,18 @@ dependencies = [ "rustversion", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -149,7 +205,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -187,6 +243,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -198,16 +263,32 @@ name = "auth-proxy" version = "0.1.0" dependencies = [ "async-trait", + "base64 0.22.1", "color-eyre", + "compact_jwt", + "cookie-rs", + "ed25519-dalek", + "form_urlencoded", "http", + "jiff", + "papaya", "pingora", + "postcard", "prost", "prost-reflect", "prost-reflect-build", + "rand 0.8.5", + "reqwest", "rustls", + "serde", + "serde_html_form", + "serde_json", + "sha2 0.10.9", "tokio", "tracing", "tracing-subscriber", + "url", + "uuid", ] [[package]] @@ -253,12 +334,41 @@ 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.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[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 = "base64urlsafedata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f7f6be94fa637132933fd0a68b9140bcb60e3d46164cb68e82a2bb8d102b3a" +dependencies = [ + "base64 0.21.7", + "pastey", + "serde", +] + [[package]] name = "beef" version = "0.5.2" @@ -283,7 +393,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -295,6 +405,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "brotli" version = "3.5.0" @@ -327,6 +455,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "byteorder" version = "1.5.0" @@ -339,6 +473,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.55" @@ -351,6 +494,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cf-rustracing" version = "1.2.1" @@ -385,6 +534,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.43" @@ -394,6 +549,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + [[package]] name = "clap" version = "4.5.57" @@ -443,6 +608,21 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -476,6 +656,47 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_jwt" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08bd132a4de34edb8e03618d1de2173571bbd2150beb2324e1ba817a4c798bb" +dependencies = [ + "base64 0.22.1", + "base64urlsafedata", + "crypto-glue", + "hex", + "kanidm-hsm-crypto", + "serde", + "serde_json", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie-rs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69deedbff42d34777a2c86794e57629898a686d84efcc28dfa8fbb515c8884ce" + [[package]] name = "core-foundation" version = "0.9.4" @@ -486,12 +707,31 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -501,6 +741,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -516,6 +762,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" @@ -523,9 +781,110 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +dependencies = [ + "hybrid-array", + "rand_core 0.10.0", +] + +[[package]] +name = "crypto-glue" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c276323bf5cd771d8eed5a8eb7011acf74450531d01efb7f8c085d4eb2c388" +dependencies = [ + "aes", + "aes-gcm", + "aes-kw", + "argon2", + "cbc", + "cipher", + "const-oid", + "crypto-common 0.1.7", + "crypto-common 0.2.0", + "der", + "digest 0.11.0", + "ecdsa", + "elliptic-curve", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "hmac 0.13.0-rc.5", + "hybrid-array", + "kbkdf", + "p256", + "p384", + "p521", + "pkcs8", + "rand 0.8.5", + "rsa", + "rustls", + "sec1", + "sha1", + "sha2 0.10.9", + "sha2 0.11.0-rc.5", + "spki", + "subtle", + "tracing", + "uuid", + "x509-cert", + "zeroize", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" +dependencies = [ + "cmov", +] + +[[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 0.10.7", + "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 2.0.114", +] + [[package]] name = "daemonize" version = "0.5.0" @@ -576,6 +935,19 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "der-parser" version = "9.0.0" @@ -590,6 +962,17 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "deranged" version = "0.5.5" @@ -597,6 +980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -647,11 +1031,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6" +dependencies = [ + "block-buffer 0.11.0", + "crypto-common 0.2.0", + "ctutils", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -670,42 +1066,128 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" +name = "ecdsa" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] [[package]] -name = "errno" -version = "0.3.14" +name = "ed25519" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "libc", - "windows-sys 0.61.2", + "pkcs8", + "serde", + "signature", ] [[package]] -name = "eyre" -version = "0.6.12" +name = "ed25519-dalek" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "indenter", - "once_cell", + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", ] [[package]] -name = "fastrand" -version = "2.3.0" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +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 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[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" @@ -719,6 +1201,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.9" @@ -748,6 +1236,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -849,8 +1346,10 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ + "serde", "typenum", "version_check", + "zeroize", ] [[package]] @@ -860,8 +1359,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -869,11 +1370,26 @@ name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", + "wasip3", ] [[package]] @@ -888,12 +1404,33 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[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 = "h2" version = "0.4.13" @@ -913,6 +1450,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -939,6 +1485,20 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -957,6 +1517,33 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef451d73f36d8a3f93ad32c332ea01146c9650e1ec821a9b0e46c01277d544f8" +dependencies = [ + "digest 0.11.0", +] + [[package]] name = "hostname" version = "0.4.2" @@ -978,6 +1565,29 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -991,121 +1601,445 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] -name = "ident_case" -version = "1.0.1" +name = "hybrid-array" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "serde", + "typenum", + "zeroize", +] [[package]] -name = "indenter" -version = "0.3.4" +name = "hyper" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] [[package]] -name = "indexmap" -version = "1.9.3" +name = "hyper-rustls" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", ] [[package]] -name = "indexmap" -version = "2.13.0" +name = "hyper-util" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "equivalent", - "hashbrown 0.16.1", + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "icu_collections" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] [[package]] -name = "itertools" -version = "0.14.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ - "either", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "itoa" -version = "1.0.17" +name = "icu_normalizer" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] [[package]] -name = "jobserver" -version = "0.1.34" +name = "icu_normalizer_data" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] -name = "lazy_static" -version = "1.5.0" +name = "icu_properties" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] [[package]] -name = "libc" -version = "0.2.180" +name = "icu_properties_data" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] -name = "libz-ng-sys" -version = "1.1.23" +name = "icu_provider" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bf914b7dd154ca9193afec311d8e39345c1bd93b48b3faa77329f0db8f553c0" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ - "cmake", - "libc", + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] -name = "linux-raw-sys" -version = "0.11.0" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "local-ip-address" -version = "0.6.10" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ef8c257c92ade496781a32a581d43e3d512cf8ce714ecf04ea80f93ed0ff4a" -dependencies = [ - "libc", - "neli", - "windows-sys 0.61.2", -] +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] -name = "lock_api" -version = "0.4.14" +name = "idna" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "scopeguard", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "log" -version = "0.4.29" +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kanidm-hsm-crypto" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cafdd63d3c246fd7a7318de64e35d2c744ebb2c5a51a407a2985ad6fe29908" +dependencies = [ + "crypto-glue", + "serde", + "tracing", +] + +[[package]] +name = "kbkdf" +version = "0.1.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ac93c9768b8d587407881c98b0c3a5d3e3049daa73408ebe5bfb1ab1cb9c84" +dependencies = [ + "digest 0.11.0", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libz-ng-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bf914b7dd154ca9193afec311d8e39345c1bd93b48b3faa77329f0db8f553c0" +dependencies = [ + "cmake", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "local-ip-address" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ef8c257c92ade496781a32a581d43e3d512cf8ce714ecf04ea80f93ed0ff4a" +dependencies = [ + "libc", + "neli", + "windows-sys 0.61.2", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "logos" @@ -1150,6 +2084,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1174,6 +2114,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1283,6 +2229,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.2.0" @@ -1298,6 +2260,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" @@ -1305,6 +2278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1337,12 +2311,24 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "ouroboros" version = "0.18.5" @@ -1373,6 +2359,54 @@ 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 0.10.9", +] + +[[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 0.10.9", +] + +[[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 0.10.9", +] + +[[package]] +name = "papaya" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" +dependencies = [ + "equivalent", + "seize", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1397,8 +2431,34 @@ dependencies = [ ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" @@ -1501,7 +2561,7 @@ dependencies = [ "log", "nix", "once_cell", - "openssl-probe", + "openssl-probe 0.1.6", "ouroboros", "parking_lot", "percent-encoding", @@ -1666,7 +2726,7 @@ dependencies = [ "pingora-error", "ring", "rustls", - "rustls-native-certs", + "rustls-native-certs 0.7.3", "rustls-pemfile", "rustls-pki-types", "tokio-rustls", @@ -1685,12 +2745,80 @@ dependencies = [ "tokio", ] +[[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 = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "heapless", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1716,6 +2844,15 @@ dependencies = [ "syn 2.0.114", ] +[[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-macro-error-attr2" version = "2.0.0" @@ -1772,7 +2909,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1865,6 +3002,62 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -1939,6 +3132,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1977,6 +3176,56 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2010,6 +3259,27 @@ dependencies = [ "serde", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust_decimal" version = "1.40.0" @@ -2026,6 +3296,12 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[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" @@ -2078,11 +3354,23 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "openssl-probe", + "openssl-probe 0.1.6", "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", ] [[package]] @@ -2100,9 +3388,37 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs 0.8.3", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.9" @@ -2127,6 +3443,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -2142,6 +3467,20 @@ 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 = "security-framework" version = "2.11.1" @@ -2149,7 +3488,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2165,6 +3517,16 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "semver" version = "1.0.27" @@ -2201,6 +3563,32 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "serde_html_form" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0946d52b4b7e28823148aebbeceb901012c595ad737920d504fa8634bb099e6f" +dependencies = [ + "form_urlencoded", + "indexmap 2.13.0", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -2220,11 +3608,44 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fa1f336066b758b7c9df34ed049c0e693a426afe2b27ff7d5b14f410ab1a132" dependencies = [ - "base64", + "base64 0.22.1", "indexmap 2.13.0", "rust_decimal", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2250,6 +3671,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -2278,6 +3709,31 @@ 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" +dependencies = [ + "lock_api", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2340,6 +3796,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2351,6 +3816,27 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -2370,7 +3856,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -2384,6 +3879,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -2434,6 +3940,52 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "tokio" version = "1.49.0" @@ -2506,6 +4058,51 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -2596,6 +4193,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -2614,6 +4217,22 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2626,12 +4245,43 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2644,6 +4294,25 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2659,12 +4328,190 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2701,6 +4548,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2734,6 +4596,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2746,6 +4614,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2758,6 +4632,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2782,6 +4662,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2794,6 +4680,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2806,6 +4698,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2818,6 +4716,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2835,6 +4739,108 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "sha1", + "signature", + "spki", + "tls_codec", +] [[package]] name = "x509-parser" @@ -2849,7 +4855,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -2859,6 +4865,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.39" @@ -2879,11 +4908,86 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "serde", + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index b9e475d..eaf2582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,15 +5,31 @@ edition = "2024" [dependencies] async-trait = "0.1.89" +base64 = "0.22.1" color-eyre = "0.6.5" +compact_jwt = { version = "0.5.4", features = ["unsafe_release_without_verify"] } +cookie-rs = "0.4.1" +ed25519-dalek = { version = "2.2.0", features = ["rand_core", "serde"] } +form_urlencoded = "1.2.2" http = "1.4.0" +jiff = "0.2.20" +papaya = "0.2.3" pingora = { version = "0.7.0", features = ["rustls", "proxy", "lb"] } +postcard = { version = "1.1.3" } prost = "0.14.3" prost-reflect = { version = "0.16.3", features = ["derive", "text-format"] } +rand = "0.8.5" # has to match ed25519-dalek +reqwest = { version = "0.13.2", features = ["json"] } rustls = { version = "0.23.36", features = ["aws-lc-rs"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_html_form = "0.4.0" +serde_json = "1.0.149" +sha2 = "0.10.9" tokio = { version = "1.49.0", features = ["fs"] } tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +url = { version = "2.5.8", features = ["serde"] } +uuid = { version = "1.21.0", features = ["serde", "v4"] } [build-dependencies] # use prost-reflect-build & prost-reflect so that we can deserialize diff --git a/flake.lock b/flake.lock index 51eb6a7..72b932f 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1770337958, - "narHash": "sha256-YhLXsYeFN4BUnZBOU6ZAi69ycZMRrDSqEXC81QfwU70=", + "lastModified": 1771119812, + "narHash": "sha256-Nqo9LfbAz6QPEGeEcoA09u89PIbSAxrkXtLVSUq8BHY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f22526981d4d5dcbc8d4c776d09964c9bb7f902b", + "rev": "cd2664195c274d53ba5430006533f3d15ebe1f1f", "type": "github" }, "original": { @@ -42,11 +42,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1770260791, - "narHash": "sha256-ADTBfENFjRVDQMcCycyX/pAy6NFI/Ct6Mrar3gsmXI0=", + "lastModified": 1771125043, + "narHash": "sha256-ldf/s49n6rOAxl7pYLJGGS1N/assoHkCOWdEdLyNZkc=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "42ec85352e419e601775c57256a52f6d48a39906", + "rev": "4912f951a26dc8142b176be2c2ad834319dc06e8", "type": "github" }, "original": { diff --git a/src/config/format.proto b/src/config/format.proto index 2066eab..c91ec50 100644 --- a/src/config/format.proto +++ b/src/config/format.proto @@ -9,8 +9,6 @@ message Config { // bind to tcp ports, with optional tls repeated TCPBinding bind_to_tcp = 2; - // bind to unix domain sockets - repeated UDSBinding bind_to_uds = 4; // lower-level pingora config Pingora pingora = 3; @@ -20,8 +18,6 @@ message Domain { // require oidc auth if this is set optional OIDC oidc_auth = 1; - // TODO: ACME challenge hosting support natively? - // https backends repeated HTTPSBackend https = 3; // http backends @@ -55,6 +51,16 @@ message OIDC { Scopes scopes = 4; Claims claims = 5; + + // per oidc core v1-with-errata-2§3.1.3.7 point 6, we _may_ skip validation + // of the id token if it was received over tls. which it will be, in our + // case. some folks may want to be extra paranoid, but generally you either + // trust tls, or you can't trust discovery, and thus can't trust the jwks info, + // so default this to false. + bool validate_with_jwk = 6; + + // where to redirect to on logout + string logout_url = 7; } message Scopes { @@ -97,7 +103,7 @@ message ManageHeaders { // set an `X-Forwarded-Proto`-style header to the original scheme of the request optional string x_forwarded_proto = 3; // set an `X-Real-IP`-style header (i.e. _just_ the remote address) - repeated string remote_addr = 4; + optional string remote_addr = 4; // always clear these headers repeated string always_clear = 5; @@ -143,14 +149,5 @@ message TCPBinding { // tls, if desired optional TLS tls = 2; - // TODO: surface tcp options from pingora -} -message UDSBinding { - message Permissions { - uint32 mode = 1; - } - // socket path - string path = 1; - // permissions to set on the socket path - optional Permissions permissions = 2; + // TODO(feature): surface tcp options from pingora } diff --git a/src/cookies.rs b/src/cookies.rs new file mode 100644 index 0000000..dc7f5f1 --- /dev/null +++ b/src/cookies.rs @@ -0,0 +1,85 @@ +//! # Cookie handling +//! +//! cookies are stored as postcard-encoded ed25519-signed tokens, [Protocol Buffer Tokens], which +//! this is inspired by. They contain session ids, _not_ the actual access token, which are stored +//! in an in-memory session store. +//! +//! the signing key is generated on server start this does mean server restarts invalidate active +//! sessions. this is not a big deal for my current usecase, and we could probably do secret +//! handover or have configurable secrets should the need arise. +//! +//! [Protocol Buffer Tokens]: https://fly.io/blog/api-tokens-a-tedious-survey/ + +use std::marker::PhantomData; + +use base64::Engine as _; +use base64::prelude::BASE64_STANDARD; +use color_eyre::Result; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Signed { + signature: ed25519_dalek::Signature, + message: Vec, + _kind: PhantomData, +} +impl Signed { + pub fn contents(raw: &str, key: &ed25519_dalek::SigningKey) -> Result { + let raw = BASE64_STANDARD.decode(raw).map_err(drop)?; + // timing threats: technically, someone could try to discover the appropriate shape of our + // tokens by seeing if we early-return from the postcard::from_bytes message. + // this doesn't seem like a huge threat, since we're not encrypting our cookies, so the + // structure is already pretty obvious + + let envelope: Self = postcard::from_bytes(&raw).map_err(drop)?; + key.verify(&envelope.message, &envelope.signature) + .map_err(drop)?; + let (version_num, msg_raw): (u64, _) = + postcard::take_from_bytes(&envelope.message).map_err(drop)?; + // we're past the signature part, so we know this is ours. + // worst we're getting here is an attempt to check if old tokens still work, + // so we don't need to be as stressed about timing sensitivity + if version_num != MSG::VERSION { + return Err(()); + } + postcard::from_bytes(msg_raw).map_err(drop) + } +} + +impl Signed { + pub fn sign(msg: MSG, key: &ed25519_dalek::SigningKey) -> Result { + use ed25519_dalek::ed25519::signature::Signer as _; + + let raw = { + let raw = Vec::new(); + let raw = postcard::to_extend(&MSG::VERSION, raw).map_err(drop)?; + postcard::to_extend(&msg, raw).map_err(drop)? + }; + let signature = key.sign(&raw); + let envelope = Self { + signature, + message: raw, + _kind: Default::default(), + }; + let raw = postcard::to_extend(&envelope, Vec::new()).map_err(drop)?; + Ok(BASE64_STANDARD.encode(raw)) + } +} + +pub trait Versioned { + const VERSION: u64; +} + +#[derive(Serialize, Deserialize)] +pub struct CookieMessage { + // NB: per rfc:draft-ietf-oauth-v2-1#7.1.3.4, since we're signing our cookies and not encrypting + // them, we can't store the access token directly. instead we'll store the session id, which + // we'll use to look up the token in a session store. + pub session_id: u64, +} +impl Versioned for CookieMessage { + const VERSION: u64 = 1; +} + +pub type CookieContents = Signed; diff --git a/src/gateway.rs b/src/gateway.rs new file mode 100644 index 0000000..ca48c96 --- /dev/null +++ b/src/gateway.rs @@ -0,0 +1,669 @@ +//! the actual gateway implementation + +use std::collections::HashMap; +use std::ops::ControlFlow; +use std::sync::Arc; + +use async_trait::async_trait; +use cookie_rs::CookieJar; +use http::status::StatusCode; +use http::{HeaderName, HeaderValue}; +use pingora::lb::selection::consistent::KetamaHashing; +use pingora::prelude::*; +use url::Url; + +use crate::gateway::oidc::{InProgressAuth, SESSION_COOKIE_NAME, UserInfo}; +use crate::httputil::{internal_error, internal_error_from, status_error, status_error_from}; +use crate::oauth::auth_code_flow; +use crate::{config, cookies, httputil}; + +pub mod oidc; + +/// per-domain information about backends and such +pub struct DomainInfo { + /// the load balancer to use to select backends + pub balancer: Arc>, + /// whether or not we allow insecure connections from clients + pub tls_mode: config::format::domain::TlsMode, + /// the sni name of this domain, used to pass to backends + pub sni_name: String, + /// auth settings for this domain, if any + pub oidc: Option, + /// headers to mangle for requests on this domain + pub headers: config::format::ManageHeaders, +} + +/// the actual gateway logic +pub struct AuthGateway { + /// all known domains and their corresponding backends & settings + pub domains: HashMap, +} + +impl AuthGateway { + /// fetch the domain info for this request + fn domain_info<'s>(&'s self, session: &Session) -> Result<&'s DomainInfo> { + let req = session.req_header(); + // TODO(potential-bug): afaict, right now, afaict, pingora a) does not check that SNI matches the `Host` + // header, b) does not support extracting the SNI info on rustls, so we'll have to switch + // to boringssl and implement that ourselves T_T + let host = req + .headers + .get(http::header::HOST) + .ok_or_else(status_error( + "no host set", + ErrorSource::Downstream, + StatusCode::BAD_REQUEST, + ))? + .to_str() + .map_err(|e| { + Error::because( + ErrorType::HTTPStatus(StatusCode::BAD_REQUEST.into()), + "no host", + e, + ) + })?; + let info = self.domains.get(host).ok_or_else(status_error( + "unknown host", + ErrorSource::Downstream, + StatusCode::SERVICE_UNAVAILABLE, + ))?; + + Ok(info) + } + + /// mangle general headers, per [`config::format::ManageHeaders`] + async fn strip_and_apply_general_headers( + &self, + session: &mut Session, + info: &DomainInfo, + is_https: bool, + ) -> Result<()> { + let remote_addr = session.client_addr().and_then(|addr| match addr { + pingora::protocols::l4::socket::SocketAddr::Inet(socket_addr) => { + Some(socket_addr.ip().to_string()) + } + pingora::protocols::l4::socket::SocketAddr::Unix(_) => None, + }); + let req = session.req_header_mut(); + if let Some(header) = &info.headers.host { + // TODO(cleanup): preprocess all header names + let name = HeaderName::from_bytes(header.as_bytes()) + .map_err(internal_error_from("invalid claim-to-header header name"))?; + let val = req + .headers + .get(http::header::HOST) + .expect("we had to have this to look up our backend") + .clone(); + req.headers.insert(name, val); + } + if let Some(header) = &info.headers.x_forwarded_for + && let Some(addr) = &remote_addr + { + let name = HeaderName::from_bytes(header.as_bytes()) + .map_err(internal_error_from("invalid claim-to-header header name"))?; + let mut val = req + .headers + .get("x-forwarded-for") + .map(|v| v.as_bytes()) + .unwrap_or(b"") + .to_owned(); + val.extend(b","); + val.extend(addr.as_bytes()); + let val = HeaderValue::from_bytes(&val) + .map_err(internal_error_from("invalid remote-addr header value"))?; + req.headers.insert(name, val); + } + if let Some(header) = &info.headers.x_forwarded_proto { + let name = HeaderName::from_bytes(header.as_bytes()) + .map_err(internal_error_from("invalid claim-to-header header name"))?; + req.headers.insert( + name, + HeaderValue::from_static(if is_https { "https" } else { "http" }), + ); + } + if let Some(header) = &info.headers.remote_addr + && let Some(addr) = &remote_addr + { + let name = HeaderName::from_bytes(header.as_bytes()) + .map_err(internal_error_from("invalid claim-to-header header name"))?; + let val = HeaderValue::from_str(addr) + .map_err(internal_error_from("invalid remote-addr header value"))?; + req.headers.insert(name, val); + } + + Ok(()) + } + + /// check auth, starting the flow if necessary + async fn check_auth( + &self, + session: &mut Session, + auth_info: &oidc::Info, + ctx: &mut AuthCtx, + ) -> Result> { + use auth_code_flow::code_request; + + let req = session.req_header_mut(); + let cookies = httputil::cookie_jar(req)?.unwrap_or_default(); + + let auth_cookie = cookies + .get(SESSION_COOKIE_NAME) + .map(|c| c.value()) + .and_then(|c| cookies::CookieContents::contents(c, &auth_info.cookie_signing_key).ok()); + { + // auth_info map pin + let sessions = auth_info.sessions.pin(); + if let Some(valid_session) = auth_cookie + .and_then(|c| sessions.get(&c.session_id)) + .filter(|sess| sess.expires_at > jiff::Timestamp::now()) + { + if let Some(claim_map) = &auth_info.config.claims { + for (claim, header) in &claim_map.claim_to_header { + match valid_session.claims.get(claim) { + Some(val) => { + let val = HeaderValue::from_bytes(val.as_bytes()) + .map_err(internal_error_from("invalid claim value"))?; + let name = HeaderName::from_bytes(header.as_bytes()).map_err( + internal_error_from("invalid claim-to-header header name"), + )?; + req.headers.insert(name, val) + } + None => req.headers.remove(header), + }; + } + } + + ctx.session_valid = true; + + return Ok(ControlFlow::Continue(())); + } + } + + // otherwise! start the auth flow + let meta_cache = auth_info.get_or_cache_metadata().await?; + + // TODO(cleanup): precompute scopes + let redirect_info = code_request::redirect_to_auth_server( + (&meta_cache.metadata).into(), + code_request::Data::new( + &auth_info.config.client_id, + &auth_info + .config + .scopes + .as_ref() + .map(|s| { + s.required + .iter() + .fold(auth_code_flow::Scopes::base_scopes(), |scopes, scope| { + scopes.add_scope(scope) + }) + }) + .unwrap_or(auth_code_flow::Scopes::base_scopes()), + // technically this is a spec violate, but it's useful for testing + &Url::parse(&format!( + "https://{domain}/{path}", + domain = auth_info.domain, + path = OAUTH_CONTINUE_PATH, + )) + .map_err(internal_error_from( + "unable to construct redirect url from domain", + ))?, + ), + ) + .map_err(internal_error_from("unable to construct redirect"))?; + + if auth_info + .auth_states + .pin() + .try_insert( + redirect_info.state, + InProgressAuth { + code_verifier: redirect_info.code_verifier, + original_path: req.uri.path().to_string(), + }, + ) + .is_err() + { + // this is _extremely_ unlikely to happen, but worth checking anyway + return Err(internal_error("state id collision")()); + }; + + httputil::redirect_response(session, redirect_info.url.as_str(), |_, _| Ok(())).await?; + Ok(ControlFlow::Break(())) + } + + /// continue auth from inbound redirects, or logout from a logout redirect + async fn receive_redirect( + &self, + session: &mut Session, + info: &DomainInfo, + ) -> Result> { + use auth_code_flow::{code_response, token_request, token_response}; + + let req = session.req_header(); + let Some(pq) = req.uri.path_and_query() else { + return Ok(ControlFlow::Continue(())); + }; + let Some(auth_info) = &info.oidc else { + return Ok(ControlFlow::Continue(())); + }; + + if pq.path() == OAUTH_LOGOUT_PATH { + let Some(mut cookies) = httputil::cookie_jar(req)? else { + // we're not logged in, just return fine + httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| Ok(())) + .await?; + return Ok(ControlFlow::Break(())); + }; + + { + let Some(raw) = cookies + .get(SESSION_COOKIE_NAME) + .map(|raw| raw.value().to_string()) + else { + // we're not logged in, just return fine + httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| { + Ok(()) + }) + .await?; + return Ok(ControlFlow::Break(())); + }; + cookies.remove(SESSION_COOKIE_NAME); + + let Some(cookies::CookieMessage { session_id }) = + cookies::CookieContents::contents(&raw, &auth_info.cookie_signing_key).ok() + else { + // invalid cookie, just ignore + httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| { + Ok(()) + }) + .await?; + return Ok(ControlFlow::Break(())); + }; + auth_info.sessions.pin().remove(&session_id); + }; + + httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| Ok(())) + .await?; + return Ok(ControlFlow::Break(())); + } + + if let Some(auth_info) = &info.oidc + && pq.path().starts_with("/") + && &pq.path()[1..] == OAUTH_CONTINUE_PATH + { + let Some(query) = pq.query() else { + session + .respond_error(StatusCode::BAD_REQUEST.into()) + .await?; + return Ok(ControlFlow::Break(())); + }; + + let Some(meta_cache) = auth_info.meta_cache.lock().await.as_ref().cloned() else { + // if we don't already have discovery metadata, something's real weird, cause + // how did we start the flow + session + .respond_error(StatusCode::BAD_REQUEST.into()) + .await?; + return Ok(ControlFlow::Break(())); + }; + + let status = + code_response::receive_redirect(query, meta_cache.metadata.issuer.as_str()) + .map_err(status_error_from( + "unable to deserialize oauth2 response", + ErrorSource::Internal, + StatusCode::BAD_REQUEST, + ))?; + let resp = match status { + Ok(resp) => resp, + Err(err) => { + auth_info.auth_states.pin().remove(&err.state); + match err.error { + code_response::ErrorType::AccessDenied => { + session.respond_error(StatusCode::FORBIDDEN.into()).await?; + return Ok(ControlFlow::Break(())); + } + code_response::ErrorType::TemporarilyUnavailable => { + session + .respond_error(StatusCode::SERVICE_UNAVAILABLE.into()) + .await?; + return Ok(ControlFlow::Break(())); + } + _ => { + session + .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into()) + .await?; + return Ok(ControlFlow::Break(())); + } + } + } + }; + + let Some(in_progress) = auth_info.auth_states.pin().remove(&resp.state).cloned() else { + session + .respond_error(StatusCode::BAD_REQUEST.into()) + .await?; + return Ok(ControlFlow::Break(())); + }; + + let mut body = String::new(); + let redirect_uri = &format!( + "https://{domain}/{path}", + domain = auth_info.domain, + path = OAUTH_CONTINUE_PATH, + ); + let token_req = token_request::request_access_token( + (&meta_cache.metadata).into(), + token_request::Data { + code: resp, + client_id: &auth_info.config.client_id, + client_secret: &auth_info.client_secret, + // this should not be a clone, but it's a weird quirk of our threadsafe + // hashmap choice + code_verifier: in_progress.code_verifier, + redirect_uri, + }, + &mut body, + ) + .map_err(internal_error_from("unable produce access token request"))?; + + let resp: token_response::Valid = { + let client = reqwest::Client::new(); + let resp = client + .post(token_req.url.as_str().to_string()) + .header( + http::header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .body(body) + .send() + .await + .map_err(internal_error_from("unable to make token request"))?; + if resp.status() == StatusCode::BAD_REQUEST { + let _resp: token_response::Error = resp + .json() + .await + .map_err(internal_error_from("unable to deserialize response"))?; + session + .respond_error(StatusCode::BAD_REQUEST.into()) + .await?; + return Ok(ControlFlow::Break(())); + // error per [the rfc][ref:draft-ietf-oauth-v2-1#3.2.4] + } else if resp.status() == StatusCode::NOT_FOUND { + // maybe it moved? try fetching the info again later + auth_info.clear_metadata_cache().await; + } else if !resp.status().is_success() { + session + .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into()) + .await?; + return Ok(ControlFlow::Break(())); + } + resp.json() + .await + .map_err(internal_error_from("unable to deserialize token response"))? + }; + + use std::str::FromStr as _; + let id_token = compact_jwt::JwtUnverified::from_str( + &resp + .id_token + .ok_or_else(internal_error("no id token in response"))?, + ) + .map_err(internal_error_from("unable to deserialize id token"))?; + + // will be some if we had the option "verify" turned on, will be none otherwise + // (will never be none if the option is on but we couldn't fetch the token) + let id_token: compact_jwt::Jwt<()> = match &meta_cache.jws_verifier { + Some(verifier) => { + use compact_jwt::JwsVerifier as _; + verifier + .verify(&id_token) + .map_err(internal_error_from("unable to verify id token"))? + } + None => { + use compact_jwt::JwsVerifier as _; + let verifier = + compact_jwt::dangernoverify::JwsDangerReleaseWithoutVerify::default(); + verifier + .verify(&id_token) + .map_err(internal_error_from("unable to deserialize id_token to jwt"))? + } + }; + + // per https://openid.net/specs/openid-connect-core-1_0-final.html#TokenResponseValidation, we _must_ to check + // - iss (must match the expected issuer) + // - aud (must match our client_id) + // - exp (must expire in the future) + if id_token + .iss + .is_none_or(|iss| iss != meta_cache.metadata.issuer.as_str()) + { + return Err(internal_error("issuer mismatch on id token")()); + } + if id_token + .aud + .is_none_or(|aud| aud != auth_info.config.client_id) + { + return Err(internal_error("audience mismatch on id token")()); + } + let expires_at = jiff::Timestamp::from_second( + id_token + .exp + .ok_or_else(internal_error("missing exp on token"))?, + ) + .map_err(internal_error_from("unable to parse exp as timestamp"))?; + if expires_at < jiff::Timestamp::now() { + session + .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into()) + .await?; + return Ok(ControlFlow::Break(())); + } + + let user_info = UserInfo { + expires_at, + claims: id_token + .claims + .into_iter() + // [`serde_json::Value`] implements display that's just "semi-infallible + // serialize" + .map(|(k, v)| (k, v.to_string())) + .collect(), + }; + let expiry = user_info.expires_at; + let mut rng = rand::rngs::OsRng; + use rand::Rng as _; + let session_id = rng.r#gen(); + auth_info.sessions.pin().insert(session_id, user_info); + let cookie = cookies::CookieContents::sign( + cookies::CookieMessage { session_id }, + &auth_info.cookie_signing_key, + ) + .map_err(|()| internal_error("unable to sign cookie")())?; + + let url = format!( + "https://{domain}{original_path}", + domain = auth_info.domain, + original_path = in_progress.original_path + ); + httputil::redirect_response(session, &url, |resp, session| { + let mut cookies = httputil::cookie_jar(session.req_header())?.unwrap_or_default(); + cookies.set( + cookie_rs::Cookie::builder(SESSION_COOKIE_NAME, cookie) + .http_only(true) + .secure(true) + // utc technically potentially different than gmt, but this is just advisory (we enforce + // elsewhere), so it's ok + .max_age( + std::time::Duration::try_from(expiry - jiff::Timestamp::now()) + .expect("formed from timestamps, can't have relative parts"), + ) + .path("/") + .build(), + ); + cookies + .as_header_values() + .into_iter() + .try_for_each(|cookie| { + let val = HeaderValue::from_bytes(cookie.as_bytes()) + .map_err(internal_error_from("bad cookie header value"))?; + + resp.append_header(http::header::SET_COOKIE, val)?; + Ok::<_, Box>(()) + })?; + Ok(()) + }) + .await?; + return Ok(ControlFlow::Break(())); + } + + Ok(ControlFlow::Continue(())) + } +} + +pub struct AuthCtx { + session_valid: bool, +} + +/// the oauth2 redirect path, without the leading slash +const OAUTH_CONTINUE_PATH: &str = ".oauth2/continue"; +/// the logout/cookie-clear path, _with_ the leading slash +const OAUTH_LOGOUT_PATH: &str = "/.oauth2/logout"; + +#[async_trait] +impl ProxyHttp for AuthGateway { + type CTX = AuthCtx; + fn new_ctx(&self) -> Self::CTX { + AuthCtx { + session_valid: false, + } + } + + async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result { + let info = self.domain_info(session)?; + + // check if we need to terminate the connection cause someone sent us an http request and we + // don't allow that + let is_https = session + .digest() + .and_then(|d| d.ssl_digest.as_ref()) + .is_some(); + if !is_https { + use config::format::domain::TlsMode; + match info.tls_mode { + TlsMode::Only => { + // we should just drop the connection, although people should really just be + // using HSTS + session.shutdown().await; + return Ok(true); + } + TlsMode::UnsafeAllowHttp => {} + } + } + + // next, check if we're in the middle of an oauth flow + match self.receive_redirect(session, info).await? { + ControlFlow::Continue(()) => {} + ControlFlow::Break(()) => return Ok(true), + } + + // finally check our actual auth state, starting the auth flow as needed + if let Some(auth_info) = &info.oidc { + match self.check_auth(session, auth_info, ctx).await? { + ControlFlow::Continue(()) => {} + ControlFlow::Break(()) => return Ok(true), + } + } + + // we're past auth and are processing as normal, proceed + self.strip_and_apply_general_headers(session, info, is_https) + .await?; + + Ok(false) + } + + async fn upstream_peer( + &self, + session: &mut Session, + _ctx: &mut Self::CTX, + ) -> Result> { + fn client_addr_key(sock_addr: &pingora::protocols::l4::socket::SocketAddr) -> Vec { + use pingora::protocols::l4::socket::SocketAddr; + match sock_addr { + SocketAddr::Inet(socket_addr) => match socket_addr { + std::net::SocketAddr::V4(v4) => Vec::from(v4.ip().octets()), + std::net::SocketAddr::V6(v6) => Vec::from(v6.ip().octets()), + }, + _ => unreachable!(), + } + } + + let backends = self.domain_info(session)?; + let backend = backends + .balancer + // NB: this means that CGNAT, other proxies, etc will? consistently hit the same + // backend, so we might wanna take that into consideration. fine for now, this is + // currently for personal use ;-) + .select( + &client_addr_key(session.client_addr().ok_or_else(status_error( + "no client address", + ErrorSource::Downstream, + StatusCode::BAD_REQUEST, + ))?), /* lb on client address */ + 256, + ) + .ok_or_else(status_error( + "no available backends", + ErrorSource::Upstream, + StatusCode::SERVICE_UNAVAILABLE, + ))?; + + let needs_tls = backend + .ext + .get::() + .map(|d| d.tls) + .unwrap_or(true); + + Ok(Box::new(HttpPeer::new( + backend, + needs_tls, + backends.sni_name.to_string(), + ))) + } + + async fn response_filter( + &self, + _session: &mut Session, + upstream_response: &mut ResponseHeader, + ctx: &mut Self::CTX, + ) -> Result<()> + where + Self::CTX: Send + Sync, + { + // if we had no valid session, clear the cookie + if !ctx.session_valid { + let mut cookies = CookieJar::default(); + cookies.remove(SESSION_COOKIE_NAME); + + cookies.as_header_values().into_iter().try_for_each(|v| { + let v = HeaderValue::from_bytes(v.as_bytes()) + .map_err(internal_error_from("invalid clear cookie header value"))?; + + upstream_response + .headers + .append(http::header::SET_COOKIE, v); + Ok::<_, Box>(()) + })?; + } + Ok(()) + } +} + +/// additional data stored in the load balancer's backend structure +/// +/// for use in [`AuthGateway::upstream_peer`] +#[derive(Clone)] +pub struct BackendData { + /// does the backend want tls + pub tls: bool, +} diff --git a/src/gateway/oidc.rs b/src/gateway/oidc.rs new file mode 100644 index 0000000..4190ca4 --- /dev/null +++ b/src/gateway/oidc.rs @@ -0,0 +1,193 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use color_eyre::eyre::Context as _; +use http::status::StatusCode; +use pingora::prelude::*; +use tokio::sync::Mutex as TokioMutex; +use url::Url; + +use crate::config; +use crate::httputil::{internal_error, internal_error_from, status_error, status_error_from}; +use crate::oauth; + +/// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#cookie_prefixes +pub const SESSION_COOKIE_NAME: &str = "__Host-Http-oauth-session"; + +/// active session user information +pub struct UserInfo { + /// when this session expires, from the id token `exp` claim + pub expires_at: jiff::Timestamp, + /// all other non-default claims attached to the id token + pub claims: HashMap, +} + +/// in-progress auth flow state +#[derive(Clone)] // only needed cause papaya +pub struct InProgressAuth { + /// the code verifier, whence the code challenge was derived + pub code_verifier: String, + /// the original path we were trying to go to on this domain, + /// for redirection once the auth flow is over + pub original_path: String, +} + +/// cache of auth server metadata and associated bits +pub struct MetadataCache { + /// the metadata itself + pub metadata: oauth::metadata::AuthServerMetadata, + /// the fetched, parsed jwks info + /// "none" means validation was disabled in our config, _NOT_ "we couldn't fetch the jwks" + pub jws_verifier: Option, +} + +/// overall auth info +pub struct Info { + /// the raw config from our config file + pub config: config::format::Oidc, + // needs to be tokio because we need to hold it across an await point + /// cache of auth server metadata + pub meta_cache: TokioMutex>>, + /// the current in-progress authorization flows, bound to states submitted to the auth serve + pub auth_states: papaya::HashMap, + /// the currently active sessions, by id from the session id cookie + pub sessions: papaya::HashMap, + /// the root domain + pub domain: String, + /// the oauth client secret + pub client_secret: String, + + /// the signing key used to sign session cookies + pub cookie_signing_key: ed25519_dalek::SigningKey, +} +impl Info { + /// clear the metadata cache + pub async fn clear_metadata_cache(&self) { + *self.meta_cache.lock().await = None; + } + /// get the metadata, or cache it (and the accompanying jwks data if needed) if it hasn't get + /// been fetched + pub async fn get_or_cache_metadata(&self) -> Result> { + let mut cache = self.meta_cache.lock().await; + if let Some(ref meta) = *cache { + return Ok(meta.clone()); + } + let discovery_url = { + let url = Url::parse(&self.config.discovery_url_base) + .map_err(internal_error_from("invalid discovery url"))?; + oauth::metadata::oidc_discovery_uri(&url) + .map_err(internal_error_from("invalid discovery url suffix"))? + }; + + let meta: oauth::metadata::AuthServerMetadata = { + let resp = reqwest::Client::new() + .get(discovery_url.as_str()) + .header(http::header::ACCEPT, "application/json") + .send() + .await + .map_err(internal_error_from("unable to fetch oauth metadata doc"))?; + + if !resp.status().is_success() { + return Err(status_error( + "unable to fetch discovery info", + ErrorSource::Internal, + StatusCode::SERVICE_UNAVAILABLE, + )()); + } + resp.json().await.map_err(internal_error_from( + "unable to deserialize oauth metadata doc", + ))? + }; + + meta.generally_as_expected().map_err(internal_error_from( + "auth server not generally as expected/required", + ))?; + + let jws_verifier = if self.config.validate_with_jwk { + if !meta + .id_token_signing_alg_values_supported + .as_ref() + .is_some_and(|s| s.contains(&oauth::metadata::SigningAlgValue::ES256)) + { + return Err(internal_error("es256 signing not supported by endpoint")()); + } + + let Some(jwks_uri) = &meta.jwks_uri else { + return Err(internal_error( + "jwks not available or es256 signing not supported by endpoint", + )()); + }; + let resp = reqwest::Client::new() + .get(jwks_uri.as_str()) + .header(http::header::ACCEPT, "application/json") + .send() + .await + .map_err(status_error_from( + "unable to fetch jwks", + ErrorSource::Internal, + StatusCode::SERVICE_UNAVAILABLE, + ))?; + if !resp.status().is_success() { + return Err(status_error( + "unable to fetch jwks", + ErrorSource::Internal, + StatusCode::SERVICE_UNAVAILABLE, + )()); + } + let jwks: compact_jwt::JwkKeySet = resp + .json() + .await + .map_err(internal_error_from("unable to deserialize jwks"))?; + // per oidc discovery v1 section 3, this either contains only signing keys, or has + // keys with a `use` option. we're going to choose to only support 1 key per use + // here and require a use anyway. this whole thing is so poorly specified. the jwt + // ecosystem really is a tire fire + Some( + jwks.keys + .iter() + .filter_map(|key| { + let compact_jwt::Jwk::EC { use_: r#use, .. } = &key else { + return None; + }; + if !r#use + .as_ref() + .is_some_and(|r#use| r#use == &compact_jwt::JwkUse::Sig) + { + return None; + } + compact_jwt::JwsEs256Verifier::try_from(key).ok() + }) + .next() + .ok_or_else(internal_error("no sig keys availabe from jwks"))?, + ) + } else { + None + }; + + Ok(cache + .insert(Arc::new(MetadataCache { + metadata: meta, + jws_verifier, + })) + .clone()) + } + + pub fn from_config(config: config::format::Oidc, domain: String) -> color_eyre::Result { + let mut rng = rand::rngs::OsRng; + // TODO(feature): check & warn on permissions here? + let client_secret = std::fs::read_to_string(&config.client_secret_path) + .context("reading client secret")? + .trim() + .to_string(); + Ok(Self { + config, + meta_cache: TokioMutex::new(None), + auth_states: Default::default(), + sessions: Default::default(), + client_secret, + + cookie_signing_key: ed25519_dalek::SigningKey::generate(&mut rng), + domain, + }) + } +} diff --git a/src/httputil.rs b/src/httputil.rs new file mode 100644 index 0000000..ad0c7f8 --- /dev/null +++ b/src/httputil.rs @@ -0,0 +1,118 @@ +//! http utilities + +use http::StatusCode; +use pingora::prelude::*; + +/// closure that returns the given status with no cause +/// +/// use with [`Option::ok_or_else`] +pub fn status_error( + why: &'static str, + src: ErrorSource, + status: StatusCode, +) -> impl FnOnce() -> Box { + move || { + Error::create( + ErrorType::HTTPStatus(status.into()), + src, + Some(why.into()), + None, + ) + } +} +/// closure that returns `500 Internal Server Error`, marked as caused by the error returned to +/// the given closure +/// +/// use with [`Result::map_err`] +pub fn internal_error_from(why: &'static str) -> impl FnOnce(E) -> Box +where + E: Into>, +{ + move |cause| { + Error::create( + ErrorType::HTTPStatus(StatusCode::INTERNAL_SERVER_ERROR.into()), + ErrorSource::Internal, + Some(why.into()), + Some(cause.into()), + ) + } +} + +/// closure that returns `500 Internal Server Error` with no cause +/// +/// use with [`Option::ok_or_else`] +pub fn internal_error(why: &'static str) -> impl FnOnce() -> Box { + move || { + Error::create( + ErrorType::HTTPStatus(StatusCode::INTERNAL_SERVER_ERROR.into()), + ErrorSource::Internal, + Some(why.into()), + None, + ) + } +} +/// closure that returns the given status, marked as caused by the error returned to the given closure +/// +/// use with [`Result::map_err`] +pub fn status_error_from( + why: &'static str, + src: ErrorSource, + status: http::StatusCode, +) -> impl FnOnce(E) -> Box +where + E: Into>, +{ + move |cause| { + Error::create( + ErrorType::HTTPStatus(status.into()), + src, + Some(why.into()), + Some(cause.into()), + ) + } +} + +/// redirect to the given location +/// +/// the given callback can be used to inject additional headers, like `set-cookie` +pub async fn redirect_response( + session: &mut Session, + to: &str, + bld_resp_header: impl FnOnce(&mut ResponseHeader, &Session) -> Result<()>, +) -> Result<()> { + session.set_keepalive(None); + session + .write_response_header( + Box::new({ + // per , any redirect is fine save 307, but HTTP 302 seems to + // be their example. + let mut resp = ResponseHeader::build(StatusCode::FOUND, Some(0))?; + resp.insert_header(http::header::LOCATION, to)?; + bld_resp_header(&mut resp, session)?; + resp + }), + true, + ) + .await?; + session.finish_body().await?; + Ok(()) +} + +/// fetch the cookies for the current request +pub fn cookie_jar(req: &'_ RequestHeader) -> Result>> { + use cookie_rs::CookieJar; + + let Some(raw) = req.headers.get(http::header::COOKIE) else { + return Ok(None); + }; + Ok(Some( + raw.to_str() + .map_err(Box::::from) + .and_then(|c| Ok(CookieJar::parse(c)?)) + .map_err(status_error_from( + "bad cookie header", + ErrorSource::Downstream, + StatusCode::BAD_REQUEST, + ))?, + )) +} diff --git a/src/main.rs b/src/main.rs index 4f68fd9..d83eafd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,183 +1,27 @@ use std::collections::HashMap; -use std::sync::Arc; -use async_trait::async_trait; use color_eyre::eyre::Context as _; -use http::status::StatusCode; use pingora::lb; use pingora::lb::selection::consistent::KetamaHashing; -use pingora::modules::http::HttpModules; -use pingora::modules::http::compression::ResponseCompressionBuilder; use pingora::prelude::*; -mod config; - -struct BackendInfo { - balancer: Arc>, - tls_mode: config::format::domain::TlsMode, - name: String, - // TODO: force ssl -} - -pub struct AuthGateway { - backends: HashMap, -} - -fn status_error( - why: &'static str, - src: ErrorSource, - status: http::StatusCode, -) -> impl FnOnce() -> Box { - move || { - Error::create( - ErrorType::HTTPStatus(status.into()), - src, - Some(why.into()), - None, - ) - } -} - -impl AuthGateway { - fn backend_info<'s>(&'s self, session: &Session) -> Result<&'s BackendInfo> { - let req = session.req_header(); - // TODO: afaict, right now, afaict, pingora a) does not check that SNI matches the `Host` - // header, b) does not support extracting the SNI info on rustls, so we'll have to switch - // to boringssl and implement that ourselves T_T - let host = req - .headers - .get(http::header::HOST) - .ok_or_else(status_error( - "no host set", - ErrorSource::Downstream, - StatusCode::BAD_REQUEST, - ))? - .to_str() - .map_err(|e| { - Error::because( - ErrorType::HTTPStatus(StatusCode::BAD_REQUEST.into()), - "no host", - e, - ) - })?; - let info = self.backends.get(host).ok_or_else(status_error( - "unknown host", - ErrorSource::Downstream, - StatusCode::SERVICE_UNAVAILABLE, - ))?; - - Ok(info) - } -} - -#[async_trait] -impl ProxyHttp for AuthGateway { - type CTX = (); - fn new_ctx(&self) -> Self::CTX {} - - fn init_downstream_modules(&self, modules: &mut HttpModules) { - // TODO: make this configurable? - modules.add_module(ResponseCompressionBuilder::enable(1)); - } - - async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result { - // check if this is http, and redirect - // TODO: maybe should be a module? - let is_https = session - .digest() - .and_then(|d| d.ssl_digest.as_ref()) - .is_some(); - if !is_https { - use config::format::domain::TlsMode; - let info = self.backend_info(session)?; - match info.tls_mode { - TlsMode::Only => { - // we should just drop the connection, although people should really just be - // using HSTS - session.shutdown().await; - return Ok(true); - } - TlsMode::UnsafeAllowHttp => {} - } - } +use self::gateway::{AuthGateway, BackendData, DomainInfo, oidc}; - Ok(false) - } - - async fn upstream_peer(&self, session: &mut Session, _ctx: &mut ()) -> Result> { - fn client_addr_key(sock_addr: &pingora::protocols::l4::socket::SocketAddr) -> Vec { - use pingora::protocols::l4::socket::SocketAddr; - match sock_addr { - SocketAddr::Inet(socket_addr) => match socket_addr { - std::net::SocketAddr::V4(v4) => Vec::from(v4.ip().octets()), - std::net::SocketAddr::V6(v6) => Vec::from(v6.ip().octets()), - }, - // TODO: this is... not a great key for hashing - SocketAddr::Unix(_socket_addr) => vec![], - } - } - - let backends = self.backend_info(session)?; - let backend = backends - .balancer - // NB: this means that CGNAT, other proxies, etc will? consistently hit the same - // backend, so we might wanna take that into consideration. fine for now, this is - // currently for personal use ;-) - .select( - &client_addr_key(session.client_addr().ok_or_else(status_error( - "no client address", - ErrorSource::Downstream, - StatusCode::BAD_REQUEST, - ))?), /* lb on client address */ - 256, - ) - .ok_or_else(status_error( - "no available backends", - ErrorSource::Upstream, - StatusCode::SERVICE_UNAVAILABLE, - ))?; - - let needs_tls = backend - .ext - .get::() - .map(|d| d.tls) - .unwrap_or(true); - - Ok(Box::new(HttpPeer::new( - backend, - needs_tls, - backends.name.to_string(), - ))) - } - - // TODO: upstream_request_filter to insert the right headers - - async fn response_filter( - &self, - _session: &mut Session, - _upstream_response: &mut ResponseHeader, - _ctx: &mut Self::CTX, - ) -> Result<()> - where - Self::CTX: Send + Sync, - { - Ok(()) - } - - // TODO: logging -} - -#[derive(Clone)] -struct BackendData { - tls: bool, -} +mod config; +mod cookies; +mod gateway; +mod httputil; +mod oauth; -fn balancer( - domains: &HashMap, -) -> color_eyre::Result<( +/// constructed load balancer, with [backend info][`BackendInfo`] to be passed to [`AuthGateway`] +type BalancerInfo = ( Vec>>, - HashMap, -)> { + HashMap, +); + +/// construct the load balancer and initialize the [backend info][`BackendInfo`] for the +/// [`AuthGateway`] +fn balancer(domains: &HashMap) -> color_eyre::Result { use lb::{self, Backend, discovery}; use pingora::protocols::l4::socket::SocketAddr; @@ -224,15 +68,20 @@ fn balancer( .collect::>() .context("constucting backends for domain")?; let backends = lb::Backends::new(discovery::Static::new(backends)); - // TODO: allow configuring healthchecks let balancer = LoadBalancer::from_backends(backends); let svc = background_service("health checking", balancer); - let info = BackendInfo { + let info = DomainInfo { balancer: svc.task(), tls_mode: config::format::domain::TlsMode::try_from(domain.tls_mode) .context("invalid tls mode")?, - name: name.clone(), + sni_name: name.clone(), + oidc: domain + .oidc_auth + .clone() + .map(|config| oidc::Info::from_config(config, name.clone())) + .transpose()?, + headers: domain.manage_headers.clone().unwrap_or_default(), }; balancers.insert(name.clone(), info); @@ -245,8 +94,6 @@ fn balancer( fn main() -> color_eyre::Result<()> { use color_eyre::eyre::eyre; - use std::os::unix::fs::PermissionsExt as _; - tracing_subscriber::fmt().init(); color_eyre::install()?; rustls::crypto::aws_lc_rs::default_provider() @@ -271,12 +118,7 @@ fn main() -> color_eyre::Result<()> { let (balancer_svcs, balancers) = balancer(&config.domains).context("setting up load balancing")?; - let mut gateway = http_proxy_service( - &server.configuration, - AuthGateway { - backends: balancers, - }, - ); + let mut gateway = http_proxy_service(&server.configuration, AuthGateway { domains: balancers }); for binding in config.bind_to_tcp { match binding.tls { Some(tls) => gateway @@ -285,14 +127,6 @@ fn main() -> color_eyre::Result<()> { None => gateway.add_tcp(&binding.addr), } } - for binding in config.bind_to_uds { - gateway.add_uds( - &binding.path, - binding - .permissions - .map(|p| std::fs::Permissions::from_mode(p.mode)), - ); - } balancer_svcs .into_iter() diff --git a/src/oauth.rs b/src/oauth.rs new file mode 100644 index 0000000..a337b45 --- /dev/null +++ b/src/oauth.rs @@ -0,0 +1,596 @@ +//! # Background +//! +//! so! there exists oauth & oidc packages. they're what kanidm uses to implement oidc. +//! unfortunately, they're poorly maintained on the client side (e.g. their reqwest bindings don't +//! work with the latest version of reqwest), and rather clunkily implemented through callbacks +//! instead of typestate, which mean they're tightly bound to exact implementation details. +//! +//! this is... annoying. so we have this instead +//! +//! # Overview +//! +//! this implementation is based on [oauth2.1]. this means, basically, it's [oauth2.0] with best +//! practices applied. +//! +//! the oidc half is based on [oidc core 1.0 + errata 2][oidc1], but with... some of the more +//! ill-advised ignorable parts duely ignored. +//! +//! [oauth2.1]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1 +//! [oauth2.0]: https://www.rfc-editor.org/rfc/rfc8414.html +//! [oidc1]: https://openid.net/specs/openid-connect-core-1_0.html + +/// # [OAuth 2.0 Authorization Server Metadata][rfc:8414] and related helpers +/// +/// most enums here also have additional values specified in the [iana registry]. +/// +/// [iana registry]: https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml +pub mod metadata { + use std::collections::HashSet; + + use serde::Deserialize; + use url::Url; + + /// [OAuth 2.0 Authorization Server Metadata][rfc:8414#2] + /// + /// see the rfc for field descriptions. + #[derive(Deserialize)] + pub struct AuthServerMetadata { + pub issuer: Url, + // > \[authorization_endpoint\] is REQUIRED unless no grant types are supported that use the + // > authorization endpoint + // + // we require the authorization flow, so we leave this as required + pub authorization_endpoint: Url, + // same here + pub token_endpoint: Url, + pub jwks_uri: Option, + pub response_types_supported: HashSet, + pub response_modes_supported: Option>, + pub grant_types_supported: Option>, + // defaults to [`TokenEndpointAuthMethod::ClientSecretBasic`], but oauth 2.1 [adds in post + // too][rfc:draft-ietf-v2.1#10.1] + pub token_endpoint_auth_methods_supported: Option>, + pub code_challenge_methods_supported: Option>, + + // per https://openid.net/specs/openid-connect-discovery-1_0.html + pub id_token_signing_alg_values_supported: Option>, + // per the spec, extra fields are defined in [OIDC Discovery 1.0 with errata + // 2][oidc-discovery-1]. + // + // the rfc also contains a bunch of extra fields that we don't use, so aren't captured + // here + // + // [oidc-discovery-1]: https://openid.net/specs/openid-connect-discovery-1_0.html + } + impl AuthServerMetadata { + /// check if this metadata conforms to our expectations of a modern oauth v2.1 & oidc core + /// v1 server + pub fn generally_as_expected(&self) -> color_eyre::Result<()> { + use color_eyre::eyre::eyre; + if !self.response_types_supported.contains(&ResponseType::Code) { + return Err(eyre!("response type `code` not supported by auth server")); + } + // if this is missing, assume query is supported + if !ResponseMode::Query.is_supported_for(self.response_modes_supported.as_ref()) { + return Err(eyre!("response mode `query` not supported by auth server")); + } + if !GrantType::AuthorizationCode.is_supported_for(self.grant_types_supported.as_ref()) { + return Err(eyre!( + "grant type `authorization_code` not supported by auth server" + )); + } + if !AuthMethod::ClientSecretPost + .is_supported_for(self.token_endpoint_auth_methods_supported.as_ref()) + { + return Err(eyre!( + "client_secret_post auth method not supported, not a valid oauth 2.1 server, and honestly not a good oauth 2.0 server either" + )); + } + if self + .code_challenge_methods_supported + .as_ref() + .is_none_or(|m| !m.contains(&CodeChallengeMethod::S256)) + { + return Err(eyre!( + "auth server does not support pkce, or does not support S256 pkce" + )); + } + if self.authorization_endpoint.host() != self.issuer.host() { + return Err(eyre!( + "authorization endpoint not on issuer server: {} vs {}", + self.authorization_endpoint.as_str(), + self.issuer.as_str() + )); + } + if self.token_endpoint.host() != self.issuer.host() { + return Err(eyre!("token endpoint not on issuer server")); + } + if self + .jwks_uri + .as_ref() + .is_some_and(|jwks_uri| jwks_uri.host() != self.issuer.host()) + { + return Err(eyre!("jwks uri not on issuer server")); + } + // the rest need to be checked if we ever use them + + // signing methods only checked if we want to actually verify the id tokens (see the + // config) + + Ok(()) + } + } + + /// [OAuth 2.0 Dynamic Client Registration response types][rfc:7591#2] + /// + /// as linked in the [`AuthServerMetadata::response_types_supported`] specification. + #[derive(Deserialize, Eq, PartialEq, Hash)] + #[serde(rename_all = "snake_case")] + pub enum ResponseType { + Code, + Token, + #[serde(untagged)] + Other(String), + } + + /// per this is a mix of [OAuth.Response], and [OAuth.Post] from the openid folks. + /// + /// + /// [OAuth.Response]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html + /// [OAuth.Post]: https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html + /// + /// as linked in the [`AuthServerMetadata::response_types_supported`] specification. + /// + /// defaults to [`ResponseMode::Query`] and [`ResponseMode::Fragment`] if missing + #[derive(Deserialize, Eq, PartialEq, Hash)] + #[serde(rename_all = "snake_case")] + pub enum ResponseMode { + Query, + Fragment, + FormPost, + #[serde(untagged)] + Other(String), + } + impl ResponseMode { + pub fn is_supported_for(&self, options: Option<&HashSet>) -> bool { + match options { + Some(opts) => opts.contains(self), + // per the field documentation (see [`Self`]) + None if *self == Self::Query => true, + None if *self == Self::Fragment => true, + None => false, + } + } + } + + /// [OAuth 2.0 Dynamic Client Registration grant types][rfc:7591#2] + /// + /// as linked in the [`AuthServerMetadata::grant_types_supported`] specification. + /// + /// defaults to [`GrantType::AuthorizationCode`] and [`GrantType::Implicit`] if missing per + /// the rfc, but oauth 2.1 [removes the implict grant][rfc:draft-ietf-v2.1#10.1] + #[derive(Deserialize, Eq, PartialEq, Hash)] + #[serde(rename_all = "snake_case")] + pub enum GrantType { + AuthorizationCode, + Implicit, + Password, + ClientCredentials, + RefreshToken, + #[serde(rename = "urn:ietf:params:oauth:grant-type:jwt-bearer")] + UrnIetfParamsOauthGrantTypeJwtBearer, + #[serde(rename = "urn:ietf:params:oauth:grant-type:saml2-bearer")] + UrnIetfParamsOauthGrantTypeSaml2Bearer, + #[serde(untagged)] + Other(String), + } + impl GrantType { + pub fn is_supported_for(&self, options: Option<&HashSet>) -> bool { + match options { + Some(opts) => opts.contains(self), + // per the field documentation (see [`Self`]) + None if *self == Self::AuthorizationCode => true, + // NB: oauth 2.1 removes the implicit grant + // None if *self == Self::Implicit => true, + None => false, + } + } + } + /// [OAuth 2.0 Dynamic Client Registration token endpoint auth methods][rfc:7591#2] + /// + /// as linked in the [`AuthServerMetadata::token_endpoint_auth_methods_supported`] specification. + #[derive(Deserialize, Eq, PartialEq, Hash)] + #[serde(rename_all = "snake_case")] + pub enum AuthMethod { + None, + ClientSecretPost, + ClientSecretBasic, + #[serde(untagged)] + Other(String), + } + impl AuthMethod { + pub fn is_supported_for(&self, options: Option<&HashSet>) -> bool { + match options { + Some(opts) => opts.contains(self), + // per the field documentation (see [`Self`]), this MUST be supported + None if *self == Self::ClientSecretBasic => true, + // per , servers MUST support this too + None if *self == Self::ClientSecretPost => true, + None => false, + } + } + } + /// JWT signing alg values + /// + /// as linked in the [`AuthServerMetadata::token_endpoint_auth_signing_alg_values_supported`] specification. + #[derive(Deserialize, Eq, PartialEq, Hash, Debug)] + pub enum SigningAlgValue { + /// NB: this must be rejected, but we capture it here to avoid it going into [`Self::Other`] + None, + RS256, + ES256, + #[serde(untagged)] + Other(String), + } + /// [PKCE challenge methods][rfc:7636#4.3] + /// + /// as linked in the [`AuthServerMetadata::code_challenge_methods_supported`] specification. + #[derive(Deserialize, Eq, PartialEq, Hash)] + pub enum CodeChallengeMethod { + /// should never be used, but we want to catch it so it doesn't go in Other + Plain, + S256, + #[serde(untagged)] + Other(String), + } + + /// get the [oidc discovery 1.0+errata 2][oidc-discovery-1] well-known endpoint for a given base + /// url + /// + /// users are expected to perform (both) issuer-id transformations themselves, if need-be + /// (per [rfc:8414#5]) + /// + /// [oidc-discovery-1]: https://openid.net/specs/openid-connect-discovery-1_0.html + pub fn oidc_discovery_uri(base_url: &Url) -> color_eyre::Result { + Ok(base_url.join(".well-known/openid-configuration")?) + } +} + +pub mod auth_code_flow { + //! [`metadata::GrantType::AuthorizationCode`] flow + //! + //! # [oauth 2.1 authorization code grant][rfc:draft-ietf-oauth-v2.1#4.1] + //! + //! 1. [`self::code_request::redirect_to_auth_server`] + //! 2. [`self::code_response::receive_redirect`] + //! 3. [`self::token_request::request_access_token`] + //! 4. deserialize response into either [`self::token_response::Valid`] or + //! [`self::token_response::Error`] + + use std::borrow::Cow; + + use serde::Deserialize; + + /// auto-join/split authorization code scopes + #[derive(Deserialize)] + #[serde(try_from = "Cow<'_, str>")] + pub struct Scopes<'u>(Cow<'u, str>); + + #[allow(clippy::infallible_try_from, reason = "required for serde")] + impl<'u> TryFrom> for Scopes<'u> { + type Error = std::convert::Infallible; + + fn try_from(value: Cow<'u, str>) -> std::result::Result { + Ok(Self(value)) + } + } + impl<'u> Scopes<'u> { + pub fn base_scopes() -> Self { + Self(Cow::Borrowed("openid")) + } + pub fn add_scope(mut self, scope: impl AsRef) -> Self { + match &mut self.0 { + Cow::Borrowed(b) => { + self.0 = format!("{b} {}", scope.as_ref()).into(); + } + Cow::Owned(v) => { + v.push(' '); + v.push_str(scope.as_ref()); + } + } + self + } + } + impl<'u, S: AsRef> FromIterator for Scopes<'u> { + fn from_iter>(iter: T) -> Self { + Self( + iter.into_iter() + .fold(String::new(), |mut acc, elem| { + if !acc.is_empty() { + acc.push(' '); + } + acc.push_str(elem.as_ref()); + acc + }) + .into(), + ) + } + } + + /// Step 1 + pub mod code_request { + + use color_eyre::Result; + use url::Url; + + use super::super::metadata::AuthServerMetadata; + use super::Scopes; + + /// data used to construct the initial authorization code browser redirect url + pub struct Data<'u> { + // owned cause it's unique every time + code_verifier: String, + client_id: &'u str, + scope: &'u Scopes<'u>, + state: uuid::Uuid, + redirect_uri: &'u Url, + } + impl<'u> Data<'u> { + pub fn new(client_id: &'u str, scope: &'u Scopes<'u>, redirect_uri: &'u Url) -> Self { + use base64::prelude::*; + use rand::Rng as _; + use sha2::Digest as _; + + let mut rng = rand::rngs::OsRng; + Self { + code_verifier: BASE64_URL_SAFE_NO_PAD + .encode(sha2::Sha256::digest(rng.r#gen::<[u8; 32]>())), + client_id, + scope, + state: uuid::Uuid::new_v4(), + redirect_uri, + } + } + } + + /// slice of [`super::metadata::AuthServerMetadata`] needed for the initial + /// [`redirect_to_auth_server`] call + pub struct Metadata<'u> { + authorization_endpoint: &'u Url, + } + impl<'u> From<&'u AuthServerMetadata> for Metadata<'u> { + fn from(orig: &'u AuthServerMetadata) -> Self { + Self { + authorization_endpoint: &orig.authorization_endpoint, + } + } + } + + /// the information required to start the authorization code flow and redirect a browser + pub struct RedirectInfo { + /// the url to send to the browser + pub url: Url, + /// the code verifier, to use when submitting the token request + pub code_verifier: String, + /// the state, to use to associate the authorization server's response back to the code + /// verifier and such + pub state: uuid::Uuid, + } + + /// construct the url used to redirect the user to the authorization server login + /// + /// take the returned state and use it to save the code verifier + pub fn redirect_to_auth_server( + meta: Metadata<'_>, + params: Data<'_>, + ) -> Result { + let mut url = meta.authorization_endpoint.clone(); + let mut query = url.query_pairs_mut(); + query.append_pair("response_type", "code"); + query.append_pair("client_id", params.client_id); + { + use base64::prelude::*; + use sha2::Digest as _; + + query.append_pair("code_challenge_method", "S256"); + let challenge = + BASE64_URL_SAFE.encode(sha2::Sha256::digest(params.code_verifier.as_bytes())); + query.append_pair("code_challenge", &challenge); + } + + // NB: there's some optional oidc parameters here, but they're mostly worth skipping + // the main one that's useful is the nonce, but pkce takes the place of that and is + // more broadly standardized in oauth2 v2.1 + + query.append_pair("redirect_uri", params.redirect_uri.as_str()); + query.append_pair("scope", ¶ms.scope.0); + query.append_pair("state", ¶ms.state.to_string()); + drop(query); + + Ok(RedirectInfo { + url, + code_verifier: params.code_verifier, + state: params.state, + }) + } + } + + /// Step 2 + pub mod code_response { + use std::borrow::Cow; + + use color_eyre::Result; + use serde::Deserialize; + + /// types of errors that a server can respond with, having failed the initial auth code request + #[derive(Deserialize)] + #[serde(rename_all = "snake_case")] + pub enum ErrorType<'u> { + // oauth 2.1 per [rfc:draft-ietf-oauth-v2-1#4.1.2.1] + InvalidRequest, + UnauthorizedClient, + AccessDenied, + UnsupportedResponseType, + InvalidScope, + ServerError, + TemporarilyUnavailable, + + // TODO(on-oss): + // [oidc-core-1](https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6) + // defines some, but we don't really care about those for now, so they can go in "other" + + // anything else a server happens to randomly shove in there + #[serde(untagged)] + #[allow( + dead_code, + reason = "gonna make use of these shortly to surface errors better" + )] + Other(Cow<'u, str>), + } + /// an error that the server responds with when an authorization code request fails + #[allow( + dead_code, + reason = "gonna make use of these shortly to surface errors better" + )] + #[derive(Deserialize)] + pub struct Error<'u> { + pub error: ErrorType<'u>, + pub error_description: Option>, + pub error_uri: Option>, + pub state: uuid::Uuid, + pub iss: Option>, + } + + /// the query parameters for a successful authorization code response + pub struct Response<'u> { + pub code: Cow<'u, str>, + pub state: uuid::Uuid, + } + + pub fn receive_redirect<'u>( + query: &'u str, + issuer: &'u str, + ) -> Result, Error<'u>>> { + #[derive(Deserialize)] + #[serde(untagged)] + enum Params<'u> { + Valid { + code: Cow<'u, str>, + state: uuid::Uuid, + iss: Option>, + }, + Error(Error<'u>), + } + + let params = + Params::deserialize(serde_html_form::Deserializer::from_bytes(query.as_bytes()))?; + let (code, state, iss) = match params { + Params::Valid { code, state, iss } => (code, state, iss), + Params::Error(err) => return Ok(Err(err)), + }; + if iss.as_ref().is_some_and(|iss| iss != issuer) { + // NB: it's unlikely to happen except via a misconfiguration, but technically this + // could cause us to leak our in_progress states + // per [rfc:draft-ietf-oauth-v2-1#7.14], we _must_ validate this if present + return Err(color_eyre::eyre::eyre!("issuer mismatch")); + } + + Ok(Ok(Response { code, state })) + } + } + + /// Step 3 + pub mod token_request { + use color_eyre::Result; + use url::Url; + + use super::super::metadata::AuthServerMetadata; + use super::code_response::Response; + + /// slice of [`super::metadata::AuthServerMetadata`] needed for the initial + /// [`request_access_token`] call + pub struct Metadata<'u> { + token_endpoint: &'u Url, + } + impl<'u> From<&'u AuthServerMetadata> for Metadata<'u> { + fn from(orig: &'u AuthServerMetadata) -> Self { + Self { + token_endpoint: &orig.token_endpoint, + } + } + } + + pub struct Data<'u> { + pub code: Response<'u>, // owned, is consumed + pub client_id: &'u str, + pub client_secret: &'u str, + // grant type is hardcoded for this + pub code_verifier: String, // owned, consumed + pub redirect_uri: &'u str, + } + + pub struct Request<'u> { + pub url: &'u Url, + } + + /// Step 4: request an access token + pub fn request_access_token<'u, 'm: 'u, BODY: form_urlencoded::Target>( + meta: Metadata<'m>, + data: Data<'u>, + body: BODY, + ) -> Result> { + let mut body = form_urlencoded::Serializer::new(body); + body.append_pair("grant_type", "authorization_code"); + body.append_pair("code", &data.code.code); + body.append_pair("redirect_uri", data.redirect_uri); // oauth 2.0 only + body.append_pair("client_id", data.client_id); + body.append_pair("code_verifier", &data.code_verifier); + body.append_pair("client_secret", data.client_secret); + + Ok(Request { + url: meta.token_endpoint, + }) + } + } + + /// Step 4 + pub mod token_response { + use serde::Deserialize; + use std::borrow::Cow; + + #[derive(Deserialize)] + pub struct Valid<'u> { + /// required per + /// [oidc-core-1](https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.3.3) + /// when oidc is in play + pub id_token: Option>, + } + + #[derive(Deserialize)] + #[serde(rename_all = "snake_case")] + pub enum ErrorType<'u> { + // oauth 2.1 per [rfc:draft-ietf-oauth-v2-1#4.3.2] + InvalidRequest, + InvalidClient, + InvalidGrant, + UnauthorizedClient, + UnsupportedGrantType, + InvalidScope, + + // anything else a server happens to randomly shove in there + #[serde(untagged)] + #[allow(dead_code, reason = "deserialization purposes")] + Other(Cow<'u, str>), + } + #[derive(Deserialize)] + #[allow( + dead_code, + reason = "gonna make use of these shortly to surface errors better" + )] + pub struct Error<'u> { + pub error: ErrorType<'u>, + pub error_description: Option>, + pub error_uri: Option>, + } + } +} -- knotmirror 0.0.1