diff --git a/Cargo.lock b/Cargo.lock index 1399b91..975c6cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -166,24 +166,12 @@ dependencies = [ "tracing", ] -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - [[package]] name = "bitflags" version = "1.3.2" @@ -333,7 +321,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -357,12 +345,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "constant_time_eq" version = "0.4.2" @@ -502,18 +484,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[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", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.7" @@ -524,33 +494,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if 1.0.4", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version 0.4.1", - "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.117", -] - [[package]] name = "dashmap" version = "6.2.1" @@ -571,23 +514,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - [[package]] name = "digest" version = "0.10.7" @@ -595,9 +521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", - "subtle", ] [[package]] @@ -606,71 +530,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] - [[package]] name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -693,22 +558,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "filedescriptor" version = "0.8.3" @@ -716,7 +565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" dependencies = [ "libc", - "thiserror 1.0.69", + "thiserror", "winapi 0.3.9", ] @@ -835,7 +684,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -875,7 +724,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -902,17 +750,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - [[package]] name = "half" version = "2.7.1" @@ -963,24 +800,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "http" version = "1.4.1" @@ -1150,30 +969,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonwebtoken" -version = "11.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881733cbc631fc9e472e24447ce32a64bedf2da498d6d8570b08edc87de71f65" -dependencies = [ - "base64", - "ed25519-dalek", - "getrandom 0.2.17", - "hmac", - "js-sys", - "p256", - "p384", - "pem", - "rand", - "rsa", - "serde", - "serde_json", - "sha2", - "signature", - "simple_asn1", - "zeroize", -] - [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1189,9 +984,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "leb128fmt" @@ -1205,12 +997,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1409,58 +1195,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "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", - "smallvec 1.15.1", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-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" @@ -1468,7 +1202,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -1505,30 +1238,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - [[package]] name = "parking_lot" version = "0.9.0" @@ -1537,7 +1246,7 @@ checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" dependencies = [ "lock_api 0.3.4", "parking_lot_core 0.6.3", - "rustc_version 0.2.3", + "rustc_version", ] [[package]] @@ -1560,7 +1269,7 @@ dependencies = [ "cloudabi", "libc", "redox_syscall 0.1.57", - "rustc_version 0.2.3", + "rustc_version", "smallvec 0.6.14", "winapi 0.3.9", ] @@ -1578,25 +1287,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64", - "serde_core", -] - -[[package]] -name = "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" @@ -1615,27 +1305,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "plotters" version = "0.3.7" @@ -1691,12 +1360,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1713,16 +1376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", + "syn", ] [[package]] @@ -1782,7 +1436,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1795,7 +1449,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1917,16 +1571,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - [[package]] name = "ring" version = "0.17.14" @@ -1941,26 +1585,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rustc_version" version = "0.2.3" @@ -1970,15 +1594,6 @@ dependencies = [ "semver 0.9.0", ] -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver 1.0.28", -] - [[package]] name = "rustix" version = "1.1.4" @@ -2068,7 +1683,6 @@ dependencies = [ "dashmap", "futures 0.3.32", "futures-util", - "jsonwebtoken", "parking_lot 0.12.5", "pyo3", "pythonize", @@ -2137,20 +1751,6 @@ 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 = "3.7.0" @@ -2222,7 +1822,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2314,17 +1914,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if 1.0.4", - "cpufeatures 0.2.17", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -2366,28 +1955,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.20", - "time", -] - [[package]] name = "slab" version = "0.4.12" @@ -2419,22 +1986,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - [[package]] name = "strsim" version = "0.11.1" @@ -2458,17 +2009,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -2523,16 +2063,7 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" -dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl", ] [[package]] @@ -2543,18 +2074,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", + "syn", ] [[package]] @@ -2566,36 +2086,6 @@ dependencies = [ "cfg-if 1.0.4", ] -[[package]] -name = "time" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -2652,7 +2142,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2728,7 +2218,7 @@ checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" dependencies = [ "either", "futures-util", - "thiserror 1.0.69", + "thiserror", "tokio", ] @@ -2847,7 +2337,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2905,7 +2395,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 1.0.69", + "thiserror", "utf-8", ] @@ -3041,7 +2531,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] @@ -3171,7 +2661,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3182,7 +2672,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3327,7 +2817,7 @@ dependencies = [ "heck 0.5.0", "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3343,7 +2833,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3412,7 +2902,7 @@ checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3420,20 +2910,6 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "zmij" diff --git a/sandd/src/main.rs b/sandd/src/main.rs index 0499fa8..659fb3b 100644 --- a/sandd/src/main.rs +++ b/sandd/src/main.rs @@ -78,27 +78,9 @@ struct Args { server_url: String, /// Daemon ID (unique identifier) - /// - /// IGNORED when a token is presented: the controller registers an authenticated - /// daemon under the `sub` it verified, whatever this says. So it matters only for a - /// controller running without auth, where the UUID fallback below also applies. - /// Nebula deliberately does not set it — the token is the identity. #[arg(short, long, env = "SANDD_DAEMON_ID")] daemon_id: Option, - /// Bearer token proving this daemon's identity to the controller (a short-lived - /// EdDSA JWT minted by the Nebula manager). - /// - /// Sent as `Authorization: Bearer ` on the WebSocket upgrade. Omit it for a - /// controller running without auth; a controller WITH auth answers 401. - /// - /// Env-only, deliberately no CLI flag: an argument is world-readable through - /// /proc//cmdline, so any process on the instance — including the workload — - /// could `ps` the token out and impersonate this daemon. Nebula delivers it via a - /// 0600 root-owned env file for the same reason. - #[arg(long, env = "SANDD_TOKEN", hide = true)] - token: Option, - /// Reconnection interval in seconds #[arg(short, long, default_value = "5")] reconnect_interval: u64, @@ -201,7 +183,6 @@ async fn main() -> Result<()> { args.heartbeat_interval, labels.clone(), args.tunnel, - args.token.as_deref(), ) .await { @@ -243,7 +224,6 @@ async fn connect_and_serve( heartbeat_interval: u64, labels: HashMap, tunnel: bool, - token: Option<&str>, ) -> Result { info!("Connecting to server at {}", server_url); @@ -255,25 +235,6 @@ async fn connect_and_serve( tokio_tungstenite::tungstenite::http::HeaderValue::from_static("sandd.v1"), ); - // Authenticate at the UPGRADE, not after: a controller with auth on rejects the - // handshake with 401, so an unauthenticated daemon never gets a socket. - if let Some(token) = token { - let value = format!("Bearer {}", token); - // from_str rejects a value with control characters or non-ASCII; a mangled token - // (truncated env file, stray newline from a heredoc) must fail loudly here rather - // than being silently sent as a header the controller cannot parse. - let mut header = - tokio_tungstenite::tungstenite::http::HeaderValue::from_str(&value) - .context("SANDD_TOKEN is not a valid HTTP header value")?; - // The token is a bearer credential; keep it out of any Debug/log output of the - // request. tungstenite does not log headers today, but this makes it structural. - header.set_sensitive(true); - request - .headers_mut() - .insert(tokio_tungstenite::tungstenite::http::header::AUTHORIZATION, header); - debug!("presenting daemon token on the upgrade ({} bytes)", token.len()); - } - // Two transports, ONE serve loop (generic over the stream): // - tunnel mode: the tailnet has no kernel route in userspace-networking, so // open the TCP hop THROUGH tailscaled's SOCKS5 proxy, then run the diff --git a/server/Cargo.toml b/server/Cargo.toml index 306d422..56e8d38 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -45,17 +45,6 @@ pythonize = { version = "0.20", optional = true } # Base64 for protocol base64 = "0.22" -# Daemon-token verification (EdDSA/Ed25519 JWTs minted by the Nebula manager). -# -# use_pem (a default feature) is required: the public key arrives as a PKIX PEM string -# in SANDD_SIGNING_PUBLIC_KEY, so DecodingKey::from_ed_pem must exist. -# -# rust_crypto is not optional either — v11 requires exactly one crypto provider to be -# selected at compile time and PANICS at the first verify if none is (it cannot infer -# one). rust_crypto over aws_lc_rs because it is pure Rust: no cmake/C toolchain in the -# build, which keeps cross-compiling to musl (how the daemon side ships) unencumbered. -jsonwebtoken = { version = "11.0", features = ["rust_crypto"] } - [features] # OFF by default so `cargo build`/`cargo test` work with no interpreter in sight — # that is what CI and `make test` run. diff --git a/server/src/auth.rs b/server/src/auth.rs deleted file mode 100644 index 61e9cb9..0000000 --- a/server/src/auth.rs +++ /dev/null @@ -1,510 +0,0 @@ -//! Daemon-token verification. -//! -//! A daemon proves who it is with a short-lived EdDSA (Ed25519) JWT minted by the -//! Nebula manager at instance-provision time and delivered through the instance -//! bootstrap. This module is the verifying half. -//! -//! WHY ASYMMETRIC: verification is purely LOCAL — the public key is enough, so a -//! controller never calls back to the manager to admit a daemon (no network hop on the -//! connect path, and no dependency on the manager being reachable). The private key -//! never leaves the manager process, which matters because this controller runs in the -//! WORKLOAD's namespace: everything handed to it is readable by that namespace's -//! tenants. A shared HMAC secret could not work here — the key that verifies would also -//! mint, so any tenant who read it could forge a token for any daemon. -//! -//! WHAT A TOKEN ASSERTS, and what each claim defends against: -//! - `sub` the daemon id. The caller binds it to the id in `Register`, so a leaked -//! token cannot be used to impersonate a DIFFERENT daemon. -//! - `aud` this controller's id. One controller serves one workload, so a token -//! minted for another workload's controller is refused here even though it -//! carries a valid signature from the same manager. -//! - `iss` the minting system, so a token signed by an unrelated system whose key we -//! happen to trust cannot pass as a daemon token. -//! - `exp` bounds a leak: a stolen token stops working on its own. -//! - `kid` (header) names the key, so a rotation can hold old and new keys at once -//! instead of needing a flag day. -//! -//! The `alg` is PINNED to EdDSA rather than read from the token header. A verifier that -//! trusts the header's alg can be walked down to `"alg":"none"` — the classic JWT -//! bypass, where an attacker strips the signature and the library obligingly accepts an -//! unsigned token. - -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; -use serde::Deserialize; - -/// The claim set the manager mints. Mirrors `pkg/sandd.DaemonClaims` on the Nebula -/// side; `tenant` is optional there and absent in single-tenant deployments. -/// -/// Only the claims this controller acts on are declared. `iat`/`nbf` are validated by -/// the library without needing fields here. -#[derive(Debug, Deserialize, PartialEq, Eq)] -pub struct DaemonClaims { - /// The daemon id this token was minted for. Authoritative: the caller requires - /// `Register.daemon_id == sub`. - pub sub: String, - /// Cross-tenant scope. Optional; empty/absent in a single-tenant cluster. - #[serde(default)] - pub tenant: String, -} - -/// Why a token was refused. Kept coarse ON PURPOSE — it is what goes back to an -/// unauthenticated caller, and a precise reason ("bad signature" vs "wrong audience") -/// tells a prober which part of its forgery to fix. The detail goes to the log instead, -/// where the operator can see it and the caller cannot. -#[derive(Debug, PartialEq, Eq)] -pub enum AuthError { - /// No `Authorization: Bearer ` header, or it was malformed. - MissingToken, - /// Present but not acceptable: bad signature, wrong aud/iss, expired, unknown kid. - InvalidToken, -} - -impl AuthError { - /// The log line for this failure. Deliberately NOT the client's response body. - pub fn detail(&self) -> &'static str { - match self { - AuthError::MissingToken => "missing or malformed Authorization: Bearer header", - AuthError::InvalidToken => "token rejected", - } - } -} - -/// Verifies daemon tokens against one public key. -/// -/// Built once at startup and shared: `decode` needs only `&self`, so this holds no -/// per-connection state and never mutates. -pub struct TokenVerifier { - key: DecodingKey, - /// Pre-built so the aud/iss/exp rules cannot drift between call sites — there is - /// exactly one place the policy is expressed. - validation: Validation, - /// The `kid` this key answers to. Empty means "accept any kid", which is the - /// single-key case; see `verify`. - kid: String, -} - -impl TokenVerifier { - /// Builds a verifier from the PKIX PEM public key the Nebula manager hands the - /// controller (`SANDD_SIGNING_PUBLIC_KEY`), the controller's own id - /// (`SANDD_CONTROLLER_ID`, which is the only `aud` it admits), the required issuer, - /// and the key id. - /// - /// Fails on a key that will not parse rather than degrading to "accept everything": - /// a controller that admits every caller is worse than one that never started, - /// because the failure is silent and looks healthy. - pub fn new( - public_key_pem: &str, - controller_id: &str, - issuer: &str, - kid: &str, - ) -> Result { - if public_key_pem.trim().is_empty() { - return Err("public key is empty".to_string()); - } - if controller_id.trim().is_empty() { - // Without this, `aud` validation would accept the empty audience and the - // per-workload isolation boundary would silently vanish. - return Err("controller id (the required audience) is empty".to_string()); - } - let key = DecodingKey::from_ed_pem(public_key_pem.as_bytes()) - .map_err(|e| format!("public key is not a PKIX PEM Ed25519 key: {}", e))?; - - // EdDSA is pinned here, not taken from the token header — see the module docs on - // the "alg":"none" downgrade. - let mut validation = Validation::new(Algorithm::EdDSA); - validation.set_audience(&[controller_id]); - // set_issuer/set_audience only take effect because the corresponding required - // claims are enforced: a token that OMITS aud or iss must be rejected, not - // treated as trivially matching. - validation.set_issuer(&[issuer]); - validation.set_required_spec_claims(&["exp", "aud", "iss"]); - - Ok(Self { - key, - validation, - kid: kid.to_string(), - }) - } - - /// Verifies a raw token, returning its claims. - /// - /// The caller must still bind `claims.sub` to the id the daemon registers as; this - /// function proves the token is authentic and addressed to this controller, not that - /// the bearer is who it later says it is. - pub fn verify(&self, token: &str) -> Result { - // Check the kid BEFORE the signature so a rotation mismatch is distinguishable - // in the logs from a forgery — both are refused, but they need different fixes. - if !self.kid.is_empty() { - match jsonwebtoken::decode_header(token) { - Ok(header) => match header.kid { - Some(ref k) if k == &self.kid => {} - Some(ref k) => { - tracing::warn!( - "rejecting token with unknown kid {:?} (this controller holds {:?}); \ - a key rotation needs the new public key deployed here too", - k, - self.kid - ); - return Err(AuthError::InvalidToken); - } - None => { - tracing::warn!("rejecting token with no kid header"); - return Err(AuthError::InvalidToken); - } - }, - Err(e) => { - tracing::warn!("rejecting token with an unparseable header: {}", e); - return Err(AuthError::InvalidToken); - } - } - } - - match decode::(token, &self.key, &self.validation) { - Ok(data) => Ok(data.claims), - Err(e) => { - // The error kind is safe to LOG (it is what an operator needs) but must - // not reach the caller — see AuthError. - tracing::warn!("rejecting daemon token: {}", e); - Err(AuthError::InvalidToken) - } - } - } -} - -/// Extracts the bearer token from an `Authorization` header value. -/// -/// Split out so it is testable without a request, and so the scheme handling has one -/// definition. The scheme match is case-INSENSITIVE ("Bearer" per RFC 6750, but the -/// scheme is case-insensitive per RFC 7235 and clients do vary); the token itself is -/// compared byte-for-byte. -pub fn bearer_token(header: Option<&str>) -> Result<&str, AuthError> { - let value = header.ok_or(AuthError::MissingToken)?; - let rest = value - .strip_prefix("Bearer ") - .or_else(|| value.strip_prefix("bearer ")) - .ok_or(AuthError::MissingToken)?; - let token = rest.trim(); - if token.is_empty() { - return Err(AuthError::MissingToken); - } - Ok(token) -} - -#[cfg(test)] -mod tests { - use super::*; - use jsonwebtoken::{encode, EncodingKey, Header}; - use serde::Serialize; - use std::time::{SystemTime, UNIX_EPOCH}; - - // A fixed Ed25519 keypair in the exact formats the two sides exchange: PKCS#8 for - // the manager's private key, PKIX for the public key it hands the controller. - // Generated with `openssl genpkey -algorithm ed25519`. - const PRIVATE_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIM7uPMqQFHrM7SxKZmYSSDgY4KGYQVMzEc2Yb3FUJqrO\n-----END PRIVATE KEY-----\n"; - - // A DIFFERENT keypair, for the "signed by a key we do not trust" case. - const OTHER_PRIVATE_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILrLXQ+DsvT7WkQvJ3xCn5V1F0VUxfNMcM0dbT8jRMLM\n-----END PRIVATE KEY-----\n"; - - const CONTROLLER_ID: &str = "sandd-abc-uid"; - const ISSUER: &str = "nebula"; - const KID: &str = "kid-1"; - - #[derive(Serialize)] - struct TestClaims { - sub: String, - aud: String, - iss: String, - exp: u64, - iat: u64, - #[serde(skip_serializing_if = "String::is_empty")] - tenant: String, - } - - fn now() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - } - - /// Derives the PKIX public PEM from a PKCS#8 private PEM, the way the manager's - /// `Signer.PublicKeyPEM()` does — so the test exercises real key material rather - /// than a hardcoded pair that could silently drift apart. - fn public_pem(private_pem: &str) -> String { - use std::io::Write; - use std::process::{Command, Stdio}; - let mut child = Command::new("openssl") - .args(["pkey", "-pubout"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("openssl must be on PATH for these tests"); - child - .stdin - .as_mut() - .unwrap() - .write_all(private_pem.as_bytes()) - .unwrap(); - let out = child.wait_with_output().unwrap(); - assert!(out.status.success(), "openssl pkey -pubout failed"); - String::from_utf8(out.stdout).unwrap() - } - - fn verifier() -> TokenVerifier { - TokenVerifier::new(&public_pem(PRIVATE_PEM), CONTROLLER_ID, ISSUER, KID).unwrap() - } - - /// Mints a token the way the manager does, with overridable parts so each test can - /// vary exactly one thing. - fn mint( - private_pem: &str, - sub: &str, - aud: &str, - iss: &str, - kid: Option<&str>, - exp_offset: i64, - ) -> String { - let mut header = Header::new(Algorithm::EdDSA); - header.kid = kid.map(|k| k.to_string()); - let n = now(); - let claims = TestClaims { - sub: sub.to_string(), - aud: aud.to_string(), - iss: iss.to_string(), - exp: (n as i64 + exp_offset) as u64, - iat: n, - tenant: String::new(), - }; - let key = EncodingKey::from_ed_pem(private_pem.as_bytes()).unwrap(); - encode(&header, &claims, &key).unwrap() - } - - fn valid_token() -> String { - mint( - PRIVATE_PEM, - "default--my-pod", - CONTROLLER_ID, - ISSUER, - Some(KID), - 3600, - ) - } - - // The happy path: a token minted by the manager for THIS controller is admitted, - // and `sub` survives so the caller can bind it to the registering daemon id. - #[test] - fn accepts_a_token_minted_for_this_controller() { - let claims = verifier().verify(&valid_token()).unwrap(); - - assert_eq!(claims.sub, "default--my-pod"); - } - - // The isolation boundary between workloads. One controller serves one workload, so a - // token minted for a DIFFERENT controller must be refused even though its signature - // is perfectly valid — this is the check that stops a compromised workload from - // driving another workload's daemons. - #[test] - fn rejects_a_token_minted_for_another_controller() { - let token = mint( - PRIVATE_PEM, - "default--my-pod", - "sandd-SOMEONE-ELSE", - ISSUER, - Some(KID), - 3600, - ); - - assert_eq!(verifier().verify(&token), Err(AuthError::InvalidToken)); - } - - // A token signed by a key this controller does not hold. Without the signature - // check, anyone could mint their own tokens with the right aud and walk in. - #[test] - fn rejects_a_token_signed_by_an_untrusted_key() { - let token = mint( - OTHER_PRIVATE_PEM, - "default--my-pod", - CONTROLLER_ID, - ISSUER, - Some(KID), - 3600, - ); - - assert_eq!(verifier().verify(&token), Err(AuthError::InvalidToken)); - } - - // exp is what bounds a leak. A stolen token has to stop working on its own, since - // nothing revokes it. - #[test] - fn rejects_an_expired_token() { - let token = mint( - PRIVATE_PEM, - "default--my-pod", - CONTROLLER_ID, - ISSUER, - Some(KID), - -3600, // expired an hour ago - ); - - assert_eq!(verifier().verify(&token), Err(AuthError::InvalidToken)); - } - - // A valid signature from an unrelated system whose key we happen to trust must not - // pass as a daemon token. - #[test] - fn rejects_a_token_from_an_unexpected_issuer() { - let token = mint( - PRIVATE_PEM, - "default--my-pod", - CONTROLLER_ID, - "some-other-system", - Some(KID), - 3600, - ); - - assert_eq!(verifier().verify(&token), Err(AuthError::InvalidToken)); - } - - // THE classic JWT bypass: strip the signature and claim the token is unsigned. It - // must fail because the algorithm is pinned, not read from the header. - #[test] - fn rejects_an_unsigned_none_alg_token() { - // {"alg":"none","kid":"kid-1"} with the real token's claims and no signature. - let token = valid_token(); - let claims_segment = token.split('.').nth(1).unwrap(); - let header = base64_url(br#"{"alg":"none","kid":"kid-1"}"#); - let forged = format!("{}.{}.", header, claims_segment); - - assert_eq!(verifier().verify(&forged), Err(AuthError::InvalidToken)); - } - - // Tampering with the payload (e.g. swapping `sub` to another daemon's id) must - // invalidate the signature. - #[test] - fn rejects_a_tampered_payload() { - let token = valid_token(); - let parts: Vec<&str> = token.split('.').collect(); - let forged_claims = base64_url( - format!( - r#"{{"sub":"default--victim","aud":"{}","iss":"{}","exp":{},"iat":{}}}"#, - CONTROLLER_ID, - ISSUER, - now() + 3600, - now() - ) - .as_bytes(), - ); - let forged = format!("{}.{}.{}", parts[0], forged_claims, parts[2]); - - assert_eq!(verifier().verify(&forged), Err(AuthError::InvalidToken)); - } - - // A rotation the operator only half-finished: the manager mints under a new kid - // while this controller still holds the old public key. Refusing is correct, and the - // log must name both ids — otherwise this is indistinguishable from a forgery and an - // operator has nothing to act on. - #[test] - fn rejects_an_unknown_kid() { - let token = mint( - PRIVATE_PEM, - "default--my-pod", - CONTROLLER_ID, - ISSUER, - Some("kid-2"), - 3600, - ); - - assert_eq!(verifier().verify(&token), Err(AuthError::InvalidToken)); - } - - // A token carrying no kid at all cannot be matched to a key, so it is refused - // rather than optimistically tried against the only key we hold. - #[test] - fn rejects_a_missing_kid() { - let token = mint( - PRIVATE_PEM, - "default--my-pod", - CONTROLLER_ID, - ISSUER, - None, - 3600, - ); - - assert_eq!(verifier().verify(&token), Err(AuthError::InvalidToken)); - } - - // Garbage in the Authorization header must be refused, not panic. This is an - // unauthenticated code path — anything reachable from the internet gets fed junk. - #[test] - fn rejects_garbage_instead_of_panicking() { - let v = verifier(); - for junk in ["", "not-a-jwt", "a.b.c", "....", "eyJhbGciOiJFZERTQSJ9"] { - assert_eq!( - v.verify(junk), - Err(AuthError::InvalidToken), - "input {:?} must be refused", - junk - ); - } - } - - // A key that will not parse must fail construction. Starting with auth silently - // disabled would look healthy while admitting everyone. - #[test] - fn refuses_to_build_without_a_usable_key() { - assert!(TokenVerifier::new("", CONTROLLER_ID, ISSUER, KID).is_err()); - assert!(TokenVerifier::new(" \n ", CONTROLLER_ID, ISSUER, KID).is_err()); - assert!(TokenVerifier::new( - "-----BEGIN PUBLIC KEY-----\nnope\n-----END PUBLIC KEY-----\n", - CONTROLLER_ID, - ISSUER, - KID - ) - .is_err()); - // A PRIVATE key where a public one belongs: a misconfiguration worth catching - // loudly, since it means private material was routed into a tenant namespace. - assert!(TokenVerifier::new(PRIVATE_PEM, CONTROLLER_ID, ISSUER, KID).is_err()); - } - - // An empty controller id would make `aud` validation vacuous and dissolve the - // per-workload boundary, so it must be refused at construction. - #[test] - fn refuses_to_build_without_a_controller_id() { - let pem = public_pem(PRIVATE_PEM); - - assert!(TokenVerifier::new(&pem, "", ISSUER, KID).is_err()); - assert!(TokenVerifier::new(&pem, " ", ISSUER, KID).is_err()); - } - - #[test] - fn extracts_a_bearer_token() { - assert_eq!(bearer_token(Some("Bearer abc.def.ghi")), Ok("abc.def.ghi")); - // Scheme is case-insensitive per RFC 7235; clients vary. - assert_eq!(bearer_token(Some("bearer abc.def.ghi")), Ok("abc.def.ghi")); - } - - #[test] - fn rejects_a_missing_or_malformed_authorization_header() { - for header in [ - None, - Some(""), - Some("abc.def.ghi"), - Some("Basic dXNlcjpwdw=="), - Some("Bearer"), - Some("Bearer "), - ] { - assert_eq!( - bearer_token(header), - Err(AuthError::MissingToken), - "header {:?} must be refused", - header - ); - } - } - - /// Minimal base64url-no-pad encoder, so a test can hand-forge a JWT segment. - fn base64_url(raw: &[u8]) -> String { - use base64::Engine; - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(raw) - } -} diff --git a/server/src/lib.rs b/server/src/lib.rs index 285b637..954944d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -2,13 +2,12 @@ #![allow(dead_code)] #![allow(non_local_definitions)] -//! The SandD controller: the registry of connected daemons, the WebSocket server -//! they dial into, and the token verification that admits them. +//! The SandD controller: the registry of connected daemons and the WebSocket server +//! they dial into. //! //! Driven from Python: `src/python.rs` holds the pyo3 bindings maturin builds into //! `sandd._core`, behind the `python` feature so `cargo test` needs no interpreter. -pub mod auth; pub mod registry; pub mod server; diff --git a/server/src/server.rs b/server/src/server.rs index 72f342a..495933d 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -1,4 +1,3 @@ -use crate::auth::{bearer_token, AuthError, TokenVerifier}; use crate::registry::{DaemonConnection, DaemonRegistry}; use anyhow::{Context, Result}; use axum::{ @@ -18,57 +17,16 @@ use std::time::Duration; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; -/// What `/ws` needs on every upgrade: the registry to place the daemon in, and the -/// verifier to admit it. Grouped because axum's `State` is a single extractor. -/// -/// `verifier` is None when auth is DISABLED (see SandboxServer::new): the switch is the -/// presence of the verifier, not a separate boolean, so there is no way to be "auth on -/// but no key" or "key present but not enforced". -#[derive(Clone)] -struct AppState { - registry: Arc, - verifier: Option>, -} - -// Lets handlers that only need the registry keep extracting -// `State>` (as /stats does) instead of reaching through AppState. -impl axum::extract::FromRef for Arc { - fn from_ref(state: &AppState) -> Self { - state.registry.clone() - } -} - pub struct SandboxServer { registry: Arc, bind_addr: String, - verifier: Option>, } impl SandboxServer { - /// Builds a server with authentication DISABLED — every daemon that speaks - /// `sandd.v1` is admitted. This is the standalone/local-dev shape (a laptop, the - /// existing e2e stacks) where the controller is not reachable by untrusted callers. - /// - /// Under Nebula, use `with_auth`: the controller runs in the workload's namespace and - /// its Service is reachable by anything that can route to it. pub fn new(bind_addr: String) -> Self { Self { registry: Arc::new(DaemonRegistry::new()), bind_addr, - verifier: None, - } - } - - /// Builds a server that REQUIRES a valid daemon token on every `/ws` upgrade. - /// - /// Taking the verifier by value (rather than a flag plus optional key) is what makes - /// "auth enabled but unusable" unrepresentable: the caller cannot enable auth without - /// having already constructed a verifier from a real key. - pub fn with_auth(bind_addr: String, verifier: TokenVerifier) -> Self { - Self { - registry: Arc::new(DaemonRegistry::new()), - bind_addr, - verifier: Some(Arc::new(verifier)), } } @@ -85,32 +43,12 @@ impl SandboxServer { heartbeat_monitor(monitor_registry).await; }); - // State that /ws needs; /stats and /health only read the registry and get it - // from the same struct. - let state = AppState { - registry, - verifier: self.verifier, - }; - - // Say which mode we are in at startup, unmissably. An operator who believes auth - // is on when it is not has no other signal — an unauthenticated controller looks - // identical to a healthy one until someone connects to it. - if state.verifier.is_some() { - info!("daemon authentication ENABLED (every /ws upgrade requires a valid token)"); - } else { - warn!( - "daemon authentication DISABLED: any client speaking sandd.v1 will be \ - admitted. Do not run this way where the controller is reachable by \ - untrusted callers." - ); - } - // Build web server let app = Router::new() .route("/ws", get(websocket_handler)) .route("/stats", get(stats_handler)) .route("/health", get(health_handler)) - .with_state(state); + .with_state(registry); info!("Starting sandbox server on {}", self.bind_addr); @@ -124,32 +62,9 @@ impl SandboxServer { } } -/// Authenticate an upgrade request, returning the daemon id the token authorizes. -/// -/// `Ok(None)` means auth is disabled and the connection is unrestricted — the caller -/// then imposes no `sub` binding, so `Register` may claim any id (the pre-auth -/// behaviour, preserved for standalone use). -/// -/// Split out of the handler so it is unit-testable: `WebSocketUpgrade` cannot be -/// constructed without a real request, but this takes only the headers. -fn authenticate( - verifier: Option<&Arc>, - headers: &HeaderMap, -) -> Result, AuthError> { - let Some(verifier) = verifier else { - return Ok(None); - }; - let header = headers - .get(axum::http::header::AUTHORIZATION) - .and_then(|v| v.to_str().ok()); - let token = bearer_token(header)?; - let claims = verifier.verify(token)?; - Ok(Some(claims.sub)) -} - async fn websocket_handler( ws: WebSocketUpgrade, - State(state): State, + State(registry): State>, headers: HeaderMap, ) -> impl IntoResponse { // Check for WebSocket subprotocol @@ -173,38 +88,13 @@ async fn websocket_handler( .into_response(); } - // Authenticate BEFORE upgrading. An unauthenticated caller never gets a socket, so it - // cannot hold server resources or reach the message loop at all — and the rejection - // is a plain HTTP 401 it can actually understand, rather than a close frame after a - // successful handshake. - let authorized_daemon = match authenticate(state.verifier.as_ref(), &headers) { - Ok(id) => id, - Err(e) => { - // Detail to the LOG (the operator needs it); the body stays generic so a - // prober cannot learn which part of its forgery to fix. - warn!("rejecting /ws upgrade: {}", e.detail()); - return (StatusCode::UNAUTHORIZED, "unauthorized").into_response(); - } - }; - if let Some(ref id) = authorized_daemon { - info!("authenticated daemon {} at upgrade", id); - } - info!("Client negotiated protocol: {}", SUPPORTED_PROTOCOL); - let registry = state.registry.clone(); ws.protocols([SUPPORTED_PROTOCOL]) - .on_upgrade(move |socket| handle_websocket(socket, registry, authorized_daemon)) + .on_upgrade(move |socket| handle_websocket(socket, registry)) .into_response() } -/// `authorized_daemon` is the `sub` from the verified token, or None when auth is -/// disabled. When present it IS the id this connection registers as, whatever the Register -/// message claims — see `registration_id`. -async fn handle_websocket( - ws: WebSocket, - registry: Arc, - authorized_daemon: Option, -) { +async fn handle_websocket(ws: WebSocket, registry: Arc) { let (mut ws_tx, mut ws_rx) = ws.split(); // Create channel for outgoing requests (Python → Daemon) @@ -241,7 +131,7 @@ async fn handle_websocket( } }; - handle_daemon_message(message, &mut daemon_id, ®istry, &mut ws_tx, &request_tx, authorized_daemon.as_deref()).await; + handle_daemon_message(message, &mut daemon_id, ®istry, &mut ws_tx, &request_tx).await; } // Receive requests from Python (via channel) @@ -312,55 +202,23 @@ fn handle_heartbeat(id: &str, registry: &Arc) -> bool { } } -/// The id a connection registers under: the token's `sub` when authenticated, otherwise -/// the id the daemon claimed. `None` means the registration must be refused. -/// -/// The TOKEN is authoritative, and a claimed id is IGNORED when one is present — not -/// compared and rejected on mismatch, which is what this used to do. Two reasons: -/// -/// - There was nothing to disagree about. `sub` already IS the identity, so a second -/// copy in the Register payload can only be right or wrong, and being wrong failed -/// silently: a daemon whose id was not configured invented a UUID, was refused, and -/// never entered the registry — indistinguishable downstream from "no daemon -/// connected yet". Deriving the id deletes that failure mode rather than reporting it. -/// - It is not a weakening. What prevents impersonation is that registering as another -/// daemon needs a token whose `sub` IS that daemon, and minting one needs the -/// manager's private key. Taking the id from the verified token enforces that -/// directly; matching a self-reported copy only enforced it indirectly. -/// -/// `authorized == None` means auth is disabled and the claimed id is used as-is — the -/// standalone/local-dev path, where there is no token to derive an identity from. -/// -/// Split out for the same reason as handle_heartbeat: the parent needs a -/// SplitSink that cannot be built without a real socket. -fn registration_id(authorized: Option<&str>, claimed: &str) -> Option { - match authorized { - Some(sub) => Some(sub.to_string()), - // An empty id is the one thing an unauthenticated caller cannot register under: - // the registry entry would be keyed by something nothing can address. - None if claimed.is_empty() => None, - None => Some(claimed.to_string()), - } -} - async fn handle_daemon_message( message: Message, daemon_id: &mut Option, registry: &Arc, ws_tx: &mut futures_util::stream::SplitSink, request_tx: &mpsc::UnboundedSender, - authorized_daemon: Option<&str>, ) { use futures_util::SinkExt; match message { Message::Register { - daemon_id: claimed, + daemon_id: id, metadata, } => { - // The token's `sub` wins over anything the payload claims; see registration_id. - let Some(id) = registration_id(authorized_daemon, &claimed) else { - warn!("refusing registration: no daemon id, and no token to derive one from"); + // An empty id would key a registry entry that nothing can address. + if id.is_empty() { + warn!("refusing registration: no daemon id"); let nack = Message::RegisterAck { success: false, message: "no daemon id".to_string(), @@ -371,17 +229,8 @@ async fn handle_daemon_message( // daemon_id stays None, so the disconnect path removes nothing and this // connection never appears in the registry. return; - }; - - // Logged when they differ, at info: it is not a failure (the token decides), - // but it is the one clue that a daemon is misconfigured, and silence here is - // what made the previous behaviour hard to diagnose. - if !claimed.is_empty() && claimed != id { - info!( - "daemon claimed id {:?} but its token authorizes {:?}; using the token's", - claimed, id - ); } + info!("Daemon {} attempting to register", id); *daemon_id = Some(id.clone()); @@ -757,151 +606,6 @@ mod tests { ); } - // The `sub` binding. A token names ONE daemon, and that is the id the connection - // registers under — so no daemon can take over another's registry entry and receive - // its exec/logs traffic. - // - // Note what this does NOT rest on: keeping the id secret. A JWT payload is base64, - // not encrypted, so the bearer can always read its own `sub`. What stops - // impersonation is that registering as another daemon requires a token whose sub IS - // that daemon, and minting one requires the manager's private key. - #[test] - fn the_token_decides_the_registered_id() { - // Agreement is the ordinary case. - assert_eq!( - registration_id(Some("daemon-1"), "daemon-1").as_deref(), - Some("daemon-1") - ); - - // The attack: a valid token for daemon-1 used to claim daemon-2. Registration - // proceeds, but under daemon-1 — so daemon-2's traffic is never redirected. - assert_eq!( - registration_id(Some("daemon-1"), "daemon-2").as_deref(), - Some("daemon-1"), - "a claimed id must never override the token's sub" - ); - - // Near-misses resolve to the token's id too, with no trimming or case folding - // that could make a lookalike collide with the real entry. - for claimed in [ - "daemon-10", - "daemon-1 ", - " daemon-1", - "Daemon-1", - "daemon-1\n", - "", - ] { - assert_eq!( - registration_id(Some("daemon-1"), claimed).as_deref(), - Some("daemon-1"), - "claiming {:?} must still register as daemon-1", - claimed - ); - } - } - - // A daemon that was never told an id is the case this replaced: it used to invent a - // UUID and be refused, leaving nothing in the registry and no clue why. With the id - // derived from the token, an unconfigured daemon now registers correctly. - #[test] - fn an_unconfigured_daemon_still_registers_under_its_token() { - // What the daemon sends when SANDD_DAEMON_ID is unset: a random UUID. - let uuid = "3f2504e0-4f89-11d3-9a0c-0305e82c3301"; - assert_eq!( - registration_id(Some("team-ml-trainer"), uuid).as_deref(), - Some("team-ml-trainer") - ); - } - - // Auth disabled: the claimed id is used as-is, preserving standalone/local-dev - // behaviour. An empty id is refused, since nothing could address that entry. - #[test] - fn the_claimed_id_is_used_when_auth_is_disabled() { - assert_eq!( - registration_id(None, "any-daemon").as_deref(), - Some("any-daemon") - ); - assert_eq!(registration_id(None, ""), None); - } - - // Auth disabled: no Authorization header needed, and no id is bound. - #[test] - fn authenticate_admits_everyone_when_disabled() { - let headers = HeaderMap::new(); - - assert_eq!(authenticate(None, &headers), Ok(None)); - } - - // Auth enabled with no credential presented. The rejection must be MissingToken, so - // the log distinguishes "daemon predates auth / lost its token" from "forgery". - #[test] - fn authenticate_requires_a_header_when_enabled() { - let verifier = Arc::new(test_verifier()); - let headers = HeaderMap::new(); - - assert_eq!( - authenticate(Some(&verifier), &headers), - Err(AuthError::MissingToken) - ); - } - - // A garbage bearer token is refused rather than panicking. This runs BEFORE the - // upgrade, on an unauthenticated path, so it is the most exposed code in the server. - #[test] - fn authenticate_rejects_a_bogus_token() { - let verifier = Arc::new(test_verifier()); - let mut headers = HeaderMap::new(); - headers.insert( - axum::http::header::AUTHORIZATION, - "Bearer not-a-real-jwt".parse().unwrap(), - ); - - assert_eq!( - authenticate(Some(&verifier), &headers), - Err(AuthError::InvalidToken) - ); - } - - // A non-UTF8 header value must be treated as absent, not unwrapped. - #[test] - fn authenticate_survives_a_non_utf8_header() { - let verifier = Arc::new(test_verifier()); - let mut headers = HeaderMap::new(); - headers.insert( - axum::http::header::AUTHORIZATION, - axum::http::HeaderValue::from_bytes(&[0xff, 0xfe]).unwrap(), - ); - - assert_eq!( - authenticate(Some(&verifier), &headers), - Err(AuthError::MissingToken) - ); - } - - // Auth is enabled by CONSTRUCTION: with_auth requires an already-built verifier, so - // "enabled but no usable key" cannot be represented. new() is explicitly the - // unauthenticated shape. - #[test] - fn auth_mode_follows_the_constructor() { - assert!(SandboxServer::new("127.0.0.1:0".to_string()) - .verifier - .is_none()); - assert!( - SandboxServer::with_auth("127.0.0.1:0".to_string(), test_verifier()) - .verifier - .is_some() - ); - } - - /// A verifier over a throwaway key. Only used for paths that must fail before any - /// signature check, so the key never needs to match a minted token. - fn test_verifier() -> TokenVerifier { - // Generated with `openssl genpkey -algorithm ed25519 | openssl pkey -pubout`. - const PUBLIC_PEM: &str = - "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=\n-----END PUBLIC KEY-----\n"; - TokenVerifier::new(PUBLIC_PEM, "sandd-test", "nebula", "kid-1").unwrap() - } - // /stats is scraped by hand (curl | jq) when a provisioned instance's daemon never // shows up, so the FIELD NAMES are the contract — renaming one silently breaks the // reader. The pre-existing three keys are asserted alongside the new `daemons` map