diff --git a/Cargo.lock b/Cargo.lock index b464a42e610a..904cc783f5f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,18 +181,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base32" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bigdecimal" version = "0.4.6" @@ -269,9 +287,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] @@ -366,6 +384,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -415,6 +434,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + [[package]] name = "conn_pool" version = "0.1.0" @@ -443,6 +468,12 @@ dependencies = [ "tracing", ] +[[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.3.1" @@ -554,6 +585,18 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[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.6" @@ -581,6 +624,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "derive_more" version = "2.0.1" @@ -616,6 +670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -631,6 +686,45 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "divan" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0583193020b29b03682d8d33bb53a5b0f50df6daacece12ca99b904cfdcb8c4" +dependencies = [ + "cfg-if", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "divan-macros" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[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 = "edb-graphql-parser" version = "0.3.0" @@ -720,6 +814,29 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "base64ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "serde_json", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -816,6 +933,16 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "flate2" version = "1.0.35" @@ -961,7 +1088,7 @@ dependencies = [ name = "gel-auth" version = "0.1.0" dependencies = [ - "base64", + "base64 0.22.1", "constant_time_eq", "derive_more", "hex-literal", @@ -978,6 +1105,46 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "gel-jwt" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "base64ct", + "const-oid", + "constant_time_eq", + "der", + "derive_more", + "divan", + "elliptic-curve", + "hex-literal", + "hmac", + "jsonwebtoken", + "libc", + "md5", + "num-bigint-dig", + "p256", + "pem", + "pkcs1", + "pkcs8", + "pretty_assertions", + "pyo3", + "pyo3_util", + "rand", + "ring", + "rsa", + "rstest 0.24.0", + "rustls-pki-types", + "sec1", + "serde", + "serde_derive", + "serde_json", + "sha2", + "thiserror 2.0.3", + "uuid", + "zeroize", +] + [[package]] name = "gel-stream" version = "0.1.0" @@ -1010,6 +1177,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1068,6 +1236,17 @@ dependencies = [ "thiserror 2.0.3", ] +[[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 = "h2" version = "0.4.7" @@ -1247,9 +1426,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hyper" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -1542,11 +1721,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "ring", + "serde", + "serde_json", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1821,6 +2016,24 @@ dependencies = [ "serde", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "serde", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -1935,6 +2148,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[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 = "parking_lot" version = "0.12.3" @@ -1964,6 +2189,25 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[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.1" @@ -1974,7 +2218,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" name = "pgrust" version = "0.1.0" dependencies = [ - "base64", + "base64 0.22.1", "bytemuck", "captive_postgres", "clap", @@ -2049,9 +2293,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2059,6 +2303,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.31" @@ -2090,6 +2355,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -2356,6 +2630,12 @@ dependencies = [ "regex-syntax 0.8.5", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -2381,7 +2661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "async-compression", - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2432,6 +2712,16 @@ dependencies = [ "quick-error", ] +[[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.8" @@ -2449,14 +2739,34 @@ dependencies = [ [[package]] name = "roaring" -version = "0.10.6" +version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4b84ba6e838ceb47b41de5194a60244fac43d9fe03b71dbe8c5a201081d6d1" +checksum = "a652edd001c53df0b3f96a36a8dc93fce6866988efc16808235653c6bcac8bf2" dependencies = [ "bytemuck", "byteorder", ] +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rstest" version = "0.23.0" @@ -2522,6 +2832,8 @@ name = "rust_native" version = "0.1.0" dependencies = [ "conn_pool", + "gel-auth", + "gel-jwt", "http 0.1.0", "pgrust", "pyo3", @@ -2702,6 +3014,21 @@ 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", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -2789,6 +3116,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2824,6 +3161,16 @@ 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 = "simba" version = "0.8.1" @@ -2907,6 +3254,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3029,6 +3386,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "terminal_size" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "test-log" version = "0.2.16" @@ -3309,9 +3676,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-normalization" @@ -3392,9 +3759,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +dependencies = [ + "getrandom", + "serde", +] [[package]] name = "valuable" @@ -3972,6 +4343,21 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "serde", + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] [[package]] name = "zerovec" diff --git a/Cargo.toml b/Cargo.toml index 51df7775b644..898a876a0338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "rust/conn_pool", "rust/db_proto", "rust/gel-auth", + "rust/gel-jwt", "rust/gel-stream", "rust/pgrust", "rust/http", @@ -23,6 +24,7 @@ tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["registry", "env-filter"] } gel-auth = { path = "rust/gel-auth" } +gel-jwt = { path = "rust/gel-jwt" } gel-stream = { path = "rust/gel-stream" } db_proto = { path = "rust/db_proto" } captive_postgres = { path = "rust/captive_postgres" } diff --git a/edb/server/_rust_native/Cargo.toml b/edb/server/_rust_native/Cargo.toml index 934c06c97d72..b526bfd6f387 100644 --- a/edb/server/_rust_native/Cargo.toml +++ b/edb/server/_rust_native/Cargo.toml @@ -17,6 +17,8 @@ pyo3_util.workspace = true conn_pool = { workspace = true, features = [ "python_extension" ] } pgrust = { workspace = true, features = [ "python_extension" ] } http = { workspace = true, features = [ "python_extension" ] } +gel-auth = { workspace = true, features = [ "python_extension" ] } +gel-jwt = { workspace = true, features = [ "python_extension" ] } [lib] crate-type = ["lib", "cdylib"] diff --git a/edb/server/_rust_native/src/lib.rs b/edb/server/_rust_native/src/lib.rs index 931aabc2baef..d0fcc95456a6 100644 --- a/edb/server/_rust_native/src/lib.rs +++ b/edb/server/_rust_native/src/lib.rs @@ -34,6 +34,7 @@ fn _rust_native(py: Python, m: &Bound) -> PyResult<()> { add_child_module(py, m, "_conn_pool", conn_pool::python::_conn_pool)?; add_child_module(py, m, "_pg_rust", pgrust::python::_pg_rust)?; add_child_module(py, m, "_http", http::python::_http)?; + add_child_module(py, m, "_jwt", gel_jwt::python::_jwt)?; Ok(()) } diff --git a/edb/server/auth.py b/edb/server/auth.py new file mode 100644 index 000000000000..c04992478916 --- /dev/null +++ b/edb/server/auth.py @@ -0,0 +1,50 @@ +from typing import TYPE_CHECKING, Iterable, List, Optional, Any + +if TYPE_CHECKING: + class SigningCtx: + def __init__(self) -> None: ... + def set_issuer(self, issuer: str) -> None: ... + def set_audience(self, audience: str) -> None: ... + def set_expiry(self, expiry: int) -> None: ... + def set_not_before(self, not_before: int) -> None: ... + def allow(self, claim: str, values: List[str]) -> None: ... + + class JWKSet: + @staticmethod + def from_hs256_key(key: bytes) -> "JWKSet": ... + def __init__(self) -> None: ... + def generate(self, *, kid: Optional[str], kty: str) -> None: ... + def add(self, **kwargs: Any) -> None: ... + def load(self, keys: str) -> int: ... + def load_json(self, keys: str) -> int: ... + def set_issuer(self, issuer: str) -> None: ... + def set_audience(self, audience: str) -> None: ... + def set_expiry(self, expiry: int) -> None: ... + def set_not_before(self, not_before: int) -> None: ... + def allow(self, claim: str, values: List[str]) -> None: ... + def deny(self, claim: str, values: List[str]) -> None: ... + def export_pem(self, *, private_keys: bool) -> bytes: ... + def export_json(self, *, private_keys: bool) -> bytes: ... + def can_sign(self) -> bool: ... + def sign( + self, claims: dict[str, Any], *, ctx: Optional[SigningCtx] = None + ) -> str: ... + def validate(self, token: str) -> dict[str, Any]: ... + def to_json(self, *, private_keys: bool) -> str: ... + def to_pem(self, *, private_keys: bool) -> str: ... + + class JWKSetCache: + def __init__(self, expiry_seconds: int) -> None: ... + # Returns a tuple of (is_fresh, registry) + def get(self, key: str) -> tuple[bool, Optional[JWKSet]]: ... + def set(self, key: str, registry: JWKSet) -> None: ... + + def generate_gel_token( + registry: JWKSet, + *, + instances: Optional[List[str] | Iterable[str]] = None, + roles: Optional[List[str] | Iterable[str]] = None, + databases: Optional[List[str] | Iterable[str]] = None, + ) -> str: ... +else: + from edb.server._rust_native._jwt import JWKSet, JWKSetCache, generate_gel_token, SigningCtx # noqa diff --git a/rust/gel-jwt/Cargo.toml b/rust/gel-jwt/Cargo.toml new file mode 100644 index 000000000000..7ff8188e1e2c --- /dev/null +++ b/rust/gel-jwt/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "gel-jwt" +version = "0.1.0" +edition = "2021" + +[features] +python_extension = ["pyo3/extension-module"] + +[dependencies] +pyo3 = { workspace = true, optional = true } +pyo3_util.workspace = true + +# This is required to be in sync w/jsonwebtoken +rand = "0.8.5" + +md5 = "0.7.0" +sha2 = "0.10.8" +constant_time_eq = "0.3" +base64 = "0.22" +thiserror = "2" +hmac = "0.12.1" +derive_more = { version = "2", features = ["debug", "from", "display"] } + +rustls-pki-types = "1" +serde = "1" +serde_derive = "1" +serde_json = "1" +jsonwebtoken = { version = "9", default-features = false } +ring = { version = "0.17", default-features = false } +rsa = { version = "0.9.7", default-features = false, features = ["std"] } +pkcs1 = "0.7.5" +pkcs8 = "0.10.2" +sec1 = { version = "0.7.3", features = ["der", "pkcs8", "alloc"] } +pem = "3" +const-oid = { version ="0.9.6", features = ["db"] } +p256 = { version = "0.13.2", features = ["jwk"] } +base64ct = { version = "1", features = ["alloc"] } +der = "0.7.9" +libc = "0.2" +elliptic-curve = { version = "0.13.8", features = ["arithmetic"] } +num-bigint-dig = "0.8.4" +zeroize = { version = "1", features = ["derive", "serde"] } +uuid = { version = "1", features = ["v4", "serde"] } + +[dev-dependencies] +pretty_assertions = "1" +rstest = "0.24.0" +hex-literal = "0.4.1" +divan = "0.1.17" + +[[bench]] +name = "encode" +harness = false + +[lib] diff --git a/rust/gel-jwt/benches/bench-jwcrypto.py b/rust/gel-jwt/benches/bench-jwcrypto.py new file mode 100644 index 000000000000..ed7cb6a57180 --- /dev/null +++ b/rust/gel-jwt/benches/bench-jwcrypto.py @@ -0,0 +1,115 @@ +from jwcrypto import jwt, jwk +import time +import statistics + +def generate_key(key_type): + if key_type == "ES256": + return jwk.JWK.generate(kty='EC', crv='P-256') + elif key_type == "RS256": + return jwk.JWK.generate(kty='RSA', size=2048) + elif key_type == "HS256": + return jwk.JWK.generate(kty='oct', size=256) + raise ValueError(f"Unsupported key type: {key_type}") + +def benchmark_encode(key_type, iterations=100): + # Generate key outside the loop + key = generate_key(key_type) + + # Benchmark full encoding process including claims creation + times = [] + for _ in range(iterations): + start = time.perf_counter_ns() + + # Create claims and sign in the timed section + claims = {"sub": "test"} + token = jwt.JWT( + header={"alg": key_type}, + claims=claims + ) + token.make_signed_token(key) + + end = time.perf_counter_ns() + times.append(end - start) + + mean = statistics.mean(times) / 1000 # Convert to microseconds + median = statistics.median(times) / 1000 + return mean, median + +def benchmark_signing(key_type, iterations=100): + # Generate key outside the loop + key = generate_key(key_type) + claims = {"sub": "test"} + + # Benchmark signing + times = [] + for _ in range(iterations): + start = time.perf_counter_ns() + + # Signing + token = jwt.JWT( + header={"alg": key_type}, + claims=claims + ) + token.make_signed_token(key) + + end = time.perf_counter_ns() + times.append(end - start) + + mean = statistics.mean(times) / 1000 + median = statistics.median(times) / 1000 + return mean, median + +def benchmark_validation(key_type, iterations=100): + # Generate key and token outside the loop + key = generate_key(key_type) + token = jwt.JWT( + header={"alg": key_type}, + claims={"sub": "test"} + ) + token.make_signed_token(key) + token_string = token.serialize() + + # Benchmark validation + times = [] + for _ in range(iterations): + start = time.perf_counter_ns() + + # Validation + jwt.JWT(jwt=token_string, key=key) + + end = time.perf_counter_ns() + times.append(end - start) + + mean = statistics.mean(times) / 1000 + median = statistics.median(times) / 1000 + return mean, median + +def main(): + key_types = ["ES256", "RS256", "HS256"] + iterations = 100 + + print(f"Running {iterations} iterations for each algorithm") + + print("\nFull encode benchmarks (including claims creation):") + print(f"{'Algorithm':<10} | {'Mean (µs)':<12} | {'Median (µs)':<12}") + print("-" * 38) + for key_type in key_types: + mean, median = benchmark_encode(key_type, iterations) + print(f"{key_type:<10} | {mean:12.2f} | {median:12.2f}") + + print("\nSigning benchmarks (pre-created claims):") + print(f"{'Algorithm':<10} | {'Mean (µs)':<12} | {'Median (µs)':<12}") + print("-" * 38) + for key_type in key_types: + mean, median = benchmark_signing(key_type, iterations) + print(f"{key_type:<10} | {mean:12.2f} | {median:12.2f}") + + print("\nValidation benchmarks:") + print(f"{'Algorithm':<10} | {'Mean (µs)':<12} | {'Median (µs)':<12}") + print("-" * 38) + for key_type in key_types: + mean, median = benchmark_validation(key_type, iterations) + print(f"{key_type:<10} | {mean:12.2f} | {median:12.2f}") + +if __name__ == "__main__": + main() diff --git a/rust/gel-jwt/benches/encode.rs b/rust/gel-jwt/benches/encode.rs new file mode 100644 index 000000000000..c68d8e30377c --- /dev/null +++ b/rust/gel-jwt/benches/encode.rs @@ -0,0 +1,38 @@ +use std::collections::HashMap; + +use gel_jwt::{KeyType, PrivateKey, SigningContext}; + +#[divan::bench(args = [&KeyType::ES256, &KeyType::RS256, &KeyType::HS256])] +fn bench_jwt_signing(b: divan::Bencher, key_type: &KeyType) { + let key = PrivateKey::generate(None, *key_type).unwrap(); + let claims = HashMap::from([("sub".to_string(), "test".into())]); + let ctx = SigningContext::default(); + + b.bench_local(move || key.sign(claims.clone(), &ctx)); +} + +#[divan::bench(args = [&KeyType::ES256, &KeyType::RS256, &KeyType::HS256])] +fn bench_jwt_validation(b: divan::Bencher, key_type: &KeyType) { + let key = PrivateKey::generate(None, *key_type).unwrap(); + let claims = HashMap::from([("sub".to_string(), "test".into())]); + let ctx = SigningContext::default(); + let token = key.sign(claims, &ctx).unwrap(); + + b.bench_local(move || key.validate(&token, &ctx)); +} + +#[divan::bench(args = [&KeyType::ES256, &KeyType::RS256, &KeyType::HS256])] +fn bench_jwt_encode(b: divan::Bencher, key_type: &KeyType) { + let key = PrivateKey::generate(None, *key_type).unwrap(); + + b.bench_local(move || { + let claims = HashMap::from([("sub".to_string(), "test".into())]); + let ctx = SigningContext::default(); + key.sign(claims, &ctx).unwrap() + }); +} + +fn main() { + // Run registered benchmarks. + divan::main(); +} diff --git a/rust/gel-jwt/src/README.md b/rust/gel-jwt/src/README.md new file mode 100644 index 000000000000..d1e14427fd08 --- /dev/null +++ b/rust/gel-jwt/src/README.md @@ -0,0 +1,16 @@ +# JWT support + +This crate provides support for JWT tokens. + +## Key types + +HS256: symmetric key +RS256: asymmetric key (RSA 2048+ + SHA256) +ES256: asymmetric key (P-256 + SHA256) + +## Supported key formats + +HS256: raw data +RS256: PKCS1/PKCS8 PEM +ES256: SEC1/PKCS8 PEM + diff --git a/rust/gel-jwt/src/bare_key.rs b/rust/gel-jwt/src/bare_key.rs new file mode 100644 index 000000000000..1b09330b8da3 --- /dev/null +++ b/rust/gel-jwt/src/bare_key.rs @@ -0,0 +1,1309 @@ +use base64ct::Encoding; +use const_oid::db::rfc5912::{ID_EC_PUBLIC_KEY, RSA_ENCRYPTION, SECP_256_R_1}; +use der::{asn1::BitString, Any, AnyRef, Decode, Encode, SliceReader}; +use elliptic_curve::{generic_array::GenericArray, sec1::FromEncodedPoint}; +use p256::elliptic_curve::{sec1::ToEncodedPoint, JwkEcKey}; +use pem::Pem; +use pkcs1::{DecodeRsaPrivateKey, UintRef}; +use pkcs8::{ + spki::{AlgorithmIdentifier, SubjectPublicKeyInfoOwned}, + PrivateKeyInfo, +}; +use rand::{rngs::ThreadRng, Rng}; +use ring::{ + rand::SystemRandom, + signature::{RsaKeyPair, ECDSA_P256_SHA256_FIXED_SIGNING}, +}; +use rsa::{ + pkcs1::EncodeRsaPrivateKey, + traits::{PrivateKeyParts, PublicKeyParts}, + BigUint, RsaPrivateKey, +}; +use rustls_pki_types::PrivatePkcs1KeyDer; +use sec1::{EcParameters, EcPrivateKey}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, str::FromStr, vec::Vec}; + +use crate::{KeyError, KeyType, KeyValidationError}; + +const MIN_OCT_LEN_BYTES: usize = 16; +const DEFAULT_GEN_OCT_LEN_BYTES: usize = 32; +const MIN_RSA_KEY_BITS: usize = 2048; +const DEFAULT_GEN_RSA_KEY_BITS: usize = 2048; + +#[derive(zeroize::ZeroizeOnDrop, Eq, PartialEq, Clone)] +pub(crate) struct HmacKey { + key: zeroize::Zeroizing>, +} + +impl std::hash::Hash for HmacKey { + fn hash(&self, state: &mut H) { + self.key.hash(state); + } +} + +#[derive(derive_more::Debug, Serialize, Deserialize)] +pub struct SerializedKeys { + pub keys: Vec, +} + +/// Deserialize +#[derive(derive_more::Debug)] +pub enum SerializedKey { + Private(Option, BarePrivateKey), + Public(Option, BarePublicKey), + #[debug("UnknownOrInvalid({_0}, {_0}, ...)")] + UnknownOrInvalid( + #[allow(unused)] KeyError, + String, + HashMap, + ), +} + +impl<'de> serde::Deserialize<'de> for SerializedKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map: HashMap = HashMap::deserialize(deserializer)?; + let get = |k: &'static str| { + map.get(k) + .map(|s| s.as_str().unwrap_or_default()) + .unwrap_or_default() + }; + + let kty = get("kty"); + let kid = map + .get("kid") + .map(|v| v.as_str().unwrap_or_default().to_owned()); + + match kty { + "RSA" => { + // Check if private key by looking for p,q components + if map.contains_key("p") && map.contains_key("q") { + // Private key + match BarePrivateKey::from_jwt_rsa( + get("n"), + get("e"), + get("d"), + get("p"), + get("q"), + ) { + Ok(key) => Ok(SerializedKey::Private(kid, key)), + Err(e) => Ok(SerializedKey::UnknownOrInvalid(e, kty.to_string(), map)), + } + } else { + // Public key + match BarePublicKey::from_jwt_rsa(get("n"), get("e")) { + Ok(key) => Ok(SerializedKey::Public(kid, key)), + Err(e) => Ok(SerializedKey::UnknownOrInvalid(e, kty.to_string(), map)), + } + } + } + "EC" => { + // Check if private key by looking for d component + if map.contains_key("d") { + // Private key + match BarePrivateKey::from_jwt_ec(get("crv"), get("d"), get("x"), get("y")) { + Ok(key) => Ok(SerializedKey::Private(kid, key)), + Err(e) => Ok(SerializedKey::UnknownOrInvalid(e, kty.to_string(), map)), + } + } else { + // Public key + match BarePublicKey::from_jwt_ec(get("crv"), get("x"), get("y")) { + Ok(key) => Ok(SerializedKey::Public(kid, key)), + Err(e) => Ok(SerializedKey::UnknownOrInvalid(e, kty.to_string(), map)), + } + } + } + "oct" => match BarePrivateKey::from_jwt_oct(get("k")) { + Ok(key) => Ok(SerializedKey::Private(kid, key)), + Err(e) => Ok(SerializedKey::UnknownOrInvalid(e, kty.to_string(), map)), + }, + _ => Ok(SerializedKey::UnknownOrInvalid( + KeyError::UnsupportedKeyType(kty.to_string()), + kty.to_string(), + map, + )), + } + } +} + +impl serde::Serialize for SerializedKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + let b64 = |s: &[u8]| zeroize::Zeroizing::new(base64ct::Base64UrlUnpadded::encode_string(s)); + + match self { + SerializedKey::Private(kid, key) => { + let mut map = serializer.serialize_map(None)?; + match &key.inner { + BarePrivateKeyInner::RS256(key) => { + let rsa = RsaPrivateKey::from_pkcs1_der(key.secret_pkcs1_der()) + .map_err(serde::ser::Error::custom)?; + if let Some(kid) = kid { + map.serialize_entry("kid", kid)?; + } + map.serialize_entry("kty", "RSA")?; + map.serialize_entry("n", &b64(&rsa.n().to_bytes_be()))?; + map.serialize_entry("e", &b64(&rsa.e().to_bytes_be()))?; + map.serialize_entry("d", &b64(&rsa.d().to_bytes_be()))?; + + // Add dp, dq, qi + let dp = rsa + .dp() + .map(|dp| dp.to_bytes_be()) + .ok_or(serde::ser::Error::custom("RSA private key must have dp"))?; + let dq = rsa + .dq() + .map(|dq| dq.to_bytes_be()) + .ok_or(serde::ser::Error::custom("RSA private key must have dq"))?; + + map.serialize_entry("dp", &b64(&dp))?; + map.serialize_entry("dq", &b64(&dq))?; + if rsa.primes().len() == 2 { + map.serialize_entry("p", &b64(&rsa.primes()[0].to_bytes_be()))?; + map.serialize_entry("q", &b64(&rsa.primes()[1].to_bytes_be()))?; + } else { + return Err(serde::ser::Error::custom( + "RSA private key must have 2 primes", + )); + } + + // Note special handling: qi should be a positive integer. Becuase we always source + // these RSA keys from PKCS1 or RsaPrivateKey, we know that qi is always positive. + let qi = rsa + .qinv() + .ok_or(serde::ser::Error::custom("RSA private key must have qi"))?; + if qi.sign() < Default::default() { + return Err(serde::ser::Error::custom("qi must be a positive integer")); + } + let (_, qi) = qi.to_bytes_be(); + map.serialize_entry("qi", &b64(&qi))?; + } + BarePrivateKeyInner::ES256(key) => { + if let Some(kid) = kid { + map.serialize_entry("kid", kid)?; + } + map.serialize_entry("kty", "EC")?; + map.serialize_entry("crv", "P-256")?; + let public_key = key.public_key(); + let point = public_key.to_encoded_point(false); + map.serialize_entry("x", &b64(point.x().unwrap()))?; + map.serialize_entry("y", &b64(point.y().unwrap()))?; + map.serialize_entry("d", &b64(key.to_bytes().as_ref()))?; + } + BarePrivateKeyInner::HS256(key) => { + if let Some(kid) = kid { + map.serialize_entry("kid", kid)?; + } + map.serialize_entry("kty", "oct")?; + map.serialize_entry("k", &b64(&key.key))?; + } + } + map.end() + } + SerializedKey::Public(kid, key) => { + let mut map = serializer.serialize_map(None)?; + match &key.inner { + BarePublicKeyInner::RS256 { n, e } => { + if let Some(kid) = kid { + map.serialize_entry("kid", kid)?; + } + map.serialize_entry("kty", "RSA")?; + map.serialize_entry("n", &b64(&n.to_bytes_be()))?; + map.serialize_entry("e", &b64(&e.to_bytes_be()))?; + } + BarePublicKeyInner::ES256(key) => { + if let Some(kid) = kid { + map.serialize_entry("kid", kid)?; + } + map.serialize_entry("kty", "EC")?; + map.serialize_entry("crv", "P-256")?; + let point = key.to_encoded_point(false); + map.serialize_entry("x", &b64(point.x().unwrap()))?; + map.serialize_entry("y", &b64(point.y().unwrap()))?; + } + BarePublicKeyInner::HS256(key) => { + if let Some(kid) = kid { + map.serialize_entry("kid", kid)?; + } + map.serialize_entry("kty", "oct")?; + map.serialize_entry("k", &b64(&key.key))?; + } + } + map.end() + } + SerializedKey::UnknownOrInvalid(_, kty, map) => { + let mut new_map = serializer.serialize_map(None)?; + new_map.serialize_entry("kty", kty)?; + for (k, v) in map { + new_map.serialize_entry(k, v)?; + } + new_map.end() + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct BareKey { + pub(crate) inner: BareKeyInner, +} + +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) enum BareKeyInner { + Private(BarePrivateKeyInner), + Public(BarePublicKeyInner), +} + +impl BareKey { + fn from_unvalidated(inner: BareKeyInner) -> Result { + match inner { + BareKeyInner::Private(inner) => Ok(Self { + inner: BareKeyInner::Private(inner.validate()?), + }), + BareKeyInner::Public(inner) => Ok(Self { + inner: BareKeyInner::Public(inner.validate()?), + }), + } + } + + pub fn key_type(&self) -> KeyType { + match &self.inner { + BareKeyInner::Private(key) => key.key_type(), + BareKeyInner::Public(key) => key.key_type(), + } + } + + /// Load a key from a PEM-encoded string. Supported formats are PKCS1, PKCS8, + /// SEC1, and `JWT OCTAL KEY`. + pub fn from_pem(pem: &str) -> Result { + let key = parse_pem(pem)?; + Self::from_parsed_unvalidated(&key).and_then(Self::from_unvalidated) + } + + pub fn from_pem_multiple(pem: &str) -> Result>, KeyError> { + let mut keys = Vec::new(); + let pems = pem::parse_many(pem).map_err(|_| KeyError::DecodeError)?; + for pem in pems { + let key = Self::from_parsed_unvalidated(&pem).and_then(Self::from_unvalidated); + keys.push(key); + } + Ok(keys) + } + + fn from_parsed_unvalidated(pem: &Pem) -> Result { + match pem.tag() { + "JWT OCTAL KEY" => handle_oct_key(pem).map(BareKeyInner::Private), + // EC never appears in a raw "ECPublicKey" form, so treat this as + // SPKI format. + // https://www.rfc-editor.org/rfc/rfc5915 + "PUBLIC KEY" | "EC PUBLIC KEY" => handle_spki_pubkey(pem).map(BareKeyInner::Public), + "RSA PUBLIC KEY" => handle_rsa_pubkey(pem).map(BareKeyInner::Public), + "EC PRIVATE KEY" => handle_ec_key(pem.contents()).map(BareKeyInner::Private), + "RSA PRIVATE KEY" => handle_rsa_key(pem).map(BareKeyInner::Private), + "PRIVATE KEY" => handle_pkcs8_key(pem.contents()).map(BareKeyInner::Private), + tag => Err(KeyError::UnsupportedKeyType(tag.to_string())), + } + } + + pub fn try_to_public(&self) -> Result { + match &self.inner { + BareKeyInner::Private(key) => BarePublicKey::from_unvalidated(key.try_into()?), + BareKeyInner::Public(key) => Ok(BarePublicKey { inner: key.clone() }), + } + } + + pub fn try_to_private(&self) -> Result { + match &self.inner { + BareKeyInner::Private(key) => Ok(BarePrivateKey { inner: key.clone() }), + BareKeyInner::Public(_) => { + Err(KeyError::UnsupportedKeyType("No private key".to_string())) + } + } + } + + pub fn try_into_public(self) -> Result { + match &self.inner { + BareKeyInner::Private(key) => BarePublicKey::from_unvalidated(key.try_into()?), + BareKeyInner::Public(key) => Ok(BarePublicKey { inner: key.clone() }), + } + } + + pub fn try_into_private(self) -> Result { + match &self.inner { + BareKeyInner::Private(key) => Ok(BarePrivateKey { inner: key.clone() }), + BareKeyInner::Public(_) => { + Err(KeyError::UnsupportedKeyType("No private key".to_string())) + } + } + } + + pub fn to_pem(&self) -> String { + match &self.inner { + BareKeyInner::Private(key) => key.to_pem(), + BareKeyInner::Public(key) => key.to_pem(), + } + } + + pub fn clone_key(&self) -> Self { + match &self.inner { + BareKeyInner::Private(key) => BareKey { + inner: BareKeyInner::Private(key.clone()), + }, + BareKeyInner::Public(key) => BareKey { + inner: BareKeyInner::Public(key.clone()), + }, + } + } +} + +/// A bare private key contains one of the following: +/// +/// - An RSA private key +/// - An ECDSA private key (P-256) +/// - A symmetric key +#[derive(Debug, Hash, PartialEq, Eq)] +pub struct BarePrivateKey { + pub(crate) inner: BarePrivateKeyInner, +} + +impl std::fmt::Debug for BarePrivateKeyInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + BarePrivateKeyInner::RS256(_) => write!(f, "RS256(...)"), + BarePrivateKeyInner::ES256(_) => write!(f, "ES256(...)"), + BarePrivateKeyInner::HS256(_) => write!(f, "HS256(...)"), + } + } +} + +impl std::hash::Hash for BarePrivateKeyInner { + fn hash(&self, state: &mut H) { + match &self { + BarePrivateKeyInner::RS256(key) => { + let Ok(key) = RsaPrivateKey::from_pkcs1_der(key.secret_pkcs1_der()) else { + return; + }; + key.n().hash(state); + } + BarePrivateKeyInner::ES256(key) => { + let key = key.public_key(); + let point = key.to_encoded_point(false); + point.hash(state); + } + BarePrivateKeyInner::HS256(key) => key.hash(state), + } + } +} + +impl PartialEq for BarePrivateKeyInner { + fn eq(&self, other: &Self) -> bool { + match (&self, &other) { + (BarePrivateKeyInner::RS256(a), BarePrivateKeyInner::RS256(b)) => { + let Ok(a) = RsaPrivateKey::from_pkcs1_der(a.secret_pkcs1_der()) else { + return false; + }; + let Ok(b) = RsaPrivateKey::from_pkcs1_der(b.secret_pkcs1_der()) else { + return false; + }; + a.n() == b.n() && a.e() == b.e() + } + (BarePrivateKeyInner::ES256(a), BarePrivateKeyInner::ES256(b)) => { + let a = a.public_key(); + let b = b.public_key(); + let a = a.to_encoded_point(false); + let b = b.to_encoded_point(false); + a == b + } + (BarePrivateKeyInner::HS256(a), BarePrivateKeyInner::HS256(b)) => a == b, + _ => false, + } + } +} + +impl Eq for BarePrivateKeyInner {} + +pub(crate) enum BarePrivateKeyInner { + /// APIs expose PKCS1 more than PKCS8 so we can work with that + RS256(rustls_pki_types::PrivatePkcs1KeyDer<'static>), + /// Use the raw p256 secret key + ES256(p256::SecretKey), + /// Bag 'o' bytes (self-zeroing). + HS256(HmacKey), +} + +impl Clone for BarePrivateKeyInner { + fn clone(&self) -> Self { + match self { + BarePrivateKeyInner::RS256(key) => BarePrivateKeyInner::RS256(key.clone_key()), + BarePrivateKeyInner::ES256(key) => BarePrivateKeyInner::ES256(key.clone()), + BarePrivateKeyInner::HS256(key) => BarePrivateKeyInner::HS256(key.clone()), + } + } +} + +/// In debug mode, using the openssl command to generate RSA keys is much faster +/// than ring. +#[allow(unused)] +#[cfg(unix)] +fn optional_openssl_rsa_keygen(bits: usize) -> Option { + use std::process::Command; + // Try to call `openssl genrsa {bits} > /dev/null 2>&1` and then parse the stdout + // as PEM. If we fail, just return None. + let output = Command::new("openssl") + .args(["genrsa", &bits.to_string()]) + .output() + .ok()?; + if output.status.success() { + let rsa = BarePrivateKey::from_pem(&String::from_utf8(output.stdout).ok()?).ok()?; + Some(rsa) + } else { + None + } +} + +#[allow(unused)] +#[cfg(not(unix))] +fn optional_openssl_rsa_keygen(bits: usize) -> Option { + None +} + +impl BarePrivateKey { + fn from_unvalidated(inner: BarePrivateKeyInner) -> Result { + Ok(Self { + inner: inner.validate()?, + }) + } + + /// Generate a new key of the given type. This may be slow for RSA keys + /// when running without compiler optimizations. + pub fn generate(key_type: KeyType) -> Result { + match key_type { + KeyType::RS256 => { + // Because keygen is so slow in debug mode, we use openssl to generate. + #[cfg(debug_assertions)] + { + let rsa = optional_openssl_rsa_keygen(DEFAULT_GEN_RSA_KEY_BITS); + if let Some(rsa) = rsa { + return Ok(rsa); + } + } + + let key = + rsa::RsaPrivateKey::new(&mut ThreadRng::default(), DEFAULT_GEN_RSA_KEY_BITS) + .unwrap(); + let key = key.to_pkcs1_der().unwrap(); + Self::from_unvalidated(BarePrivateKeyInner::RS256(PrivatePkcs1KeyDer::from( + key.to_bytes().to_vec(), + ))) + } + KeyType::ES256 => { + let key = ring::signature::EcdsaKeyPair::generate_pkcs8( + &ECDSA_P256_SHA256_FIXED_SIGNING, + &SystemRandom::new(), + ) + .unwrap(); + Self::from_unvalidated(handle_pkcs8_key(key.as_ref())?) + } + KeyType::HS256 => { + let mut rng = ThreadRng::default(); + let mut key = zeroize::Zeroizing::new(vec![0; DEFAULT_GEN_OCT_LEN_BYTES]); + rng.fill(key.as_mut_slice()); + Self::from_unvalidated(BarePrivateKeyInner::HS256(HmacKey { key })) + } + } + } + + /// Load an ECDSA key from a JWK. + pub fn from_jwt_ec(crv: &str, d: &str, x: &str, y: &str) -> Result { + if crv != "P-256" { + return Err(KeyError::UnsupportedKeyType(crv.to_string())); + } + + // TODO: Not an ideal way to parse + let validation = |c: char| !c.is_alphanumeric() && c != '-' && c != '_'; + if x.contains(validation) || y.contains(validation) || d.contains(validation) { + return Err(KeyError::DecodeError); + } + let jwk = JwkEcKey::from_str(&format!( + r#"{{"kty":"EC","crv":"P-256","x":"{}","y":"{}","d":"{}"}}"#, + x, y, d + )) + .map_err(|_| KeyError::DecodeError)?; + + let key: p256::elliptic_curve::SecretKey = + jwk.to_secret_key().map_err(|_| KeyError::DecodeError)?; + + Self::from_unvalidated(BarePrivateKeyInner::ES256(key)) + } + + /// Load an RSA key from a JWK. + pub fn from_jwt_rsa(n: &str, e: &str, d: &str, p: &str, q: &str) -> Result { + let n = BigUint::from_bytes_be(&b64_decode(n)?); + let e = BigUint::from_bytes_be(&b64_decode(e)?); + let d = BigUint::from_bytes_be(&b64_decode(d)?); + let p = BigUint::from_bytes_be(&b64_decode(p)?); + let q = BigUint::from_bytes_be(&b64_decode(q)?); + let primes = vec![p, q]; + + let rsa = rsa::RsaPrivateKey::from_components(n, e, d, primes) + .map_err(|_| KeyError::DecodeError)?; + let key = rsa + .to_pkcs1_der() + .to_owned() + .map_err(|_| KeyError::DecodeError)?; + Self::from_unvalidated(BarePrivateKeyInner::RS256(PrivatePkcs1KeyDer::from( + key.to_bytes().to_vec(), + ))) + } + + /// Load an HMAC key from a base64-encoded string. + pub fn from_jwt_oct(k: &str) -> Result { + let key = b64_decode(k)?; + Self::from_unvalidated(BarePrivateKeyInner::HS256(HmacKey { key })) + } + + /// Load an HMAC key from a raw byte slice. + pub fn from_raw_oct(key: &[u8]) -> Result { + Self::from_unvalidated(BarePrivateKeyInner::HS256(HmacKey { + key: key.to_vec().into(), + })) + } + + /// Load a key from a PEM-encoded string. Supported formats are PKCS1, PKCS8, + /// SEC1, and `JWT OCTAL KEY`. + pub fn from_pem(pem: &str) -> Result { + BareKey::from_pem(pem)?.try_into_private() + } + + pub fn from_pem_multiple(pem: &str) -> Result>, KeyError> { + Ok(BareKey::from_pem_multiple(pem)? + .into_iter() + .map(|key| key.and_then(|k| k.try_into_private())) + .collect()) + } + + pub fn to_public(&self) -> Result { + let inner = (&(self.inner)).try_into()?; + Ok(BarePublicKey { inner }) + } + + pub fn into_public(self) -> Result { + let inner = (&(self.inner)).try_into()?; + Ok(BarePublicKey { inner }) + } + + pub fn clone_key(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + + pub fn to_pem(&self) -> String { + self.inner.to_pem() + } + + pub fn to_pem_public(&self) -> Result { + self.inner.to_pem_public() + } + + pub fn key_type(&self) -> KeyType { + self.inner.key_type() + } +} + +impl BarePrivateKeyInner { + pub fn key_type(&self) -> KeyType { + match &self { + BarePrivateKeyInner::RS256(..) => KeyType::RS256, + BarePrivateKeyInner::ES256(..) => KeyType::ES256, + BarePrivateKeyInner::HS256(..) => KeyType::HS256, + } + } + + pub fn to_pem(&self) -> String { + let key = match &self { + BarePrivateKeyInner::RS256(key) => { + pem::encode(&Pem::new("RSA PRIVATE KEY", key.secret_pkcs1_der())) + } + BarePrivateKeyInner::ES256(key) => { + let pkcs8 = pkcs8_from_ec(key).unwrap(); + pem::encode(&Pem::new("PRIVATE KEY", pkcs8)) + } + BarePrivateKeyInner::HS256(key) => { + pem::encode(&Pem::new("JWT OCTAL KEY", key.key.as_slice())) + } + }; + key + } + + /// Export this private key to a public key in PEM format. + pub fn to_pem_public(&self) -> Result { + let key = match &self { + BarePrivateKeyInner::RS256(key) => { + let pkcs1 = pkcs1::RsaPrivateKey::from_der(key.secret_pkcs1_der()) + .map_err(|_| KeyError::DecodeError)?; + BarePublicKeyInner::RS256 { + n: BigUint::from_bytes_be(pkcs1.modulus.as_bytes()), + e: BigUint::from_bytes_be(pkcs1.public_exponent.as_bytes()), + } + .to_pem() + } + BarePrivateKeyInner::ES256(key) => BarePublicKeyInner::ES256(key.public_key()).to_pem(), + _ => return Err(KeyError::UnsupportedKeyType(self.key_type().to_string())), + }; + Ok(key) + } + + fn validate(self) -> Result { + match &self { + BarePrivateKeyInner::RS256(key) => { + validate_rsa_key_pair(key.secret_pkcs1_der())?; + Ok(self) + } + BarePrivateKeyInner::ES256(key) => { + validate_ecdsa_key_pair(key)?; + Ok(self) + } + BarePrivateKeyInner::HS256(key) => { + if key.key.len() < MIN_OCT_LEN_BYTES { + return Err(KeyError::UnsupportedKeyType(format!( + "oct key ({} bytes) < {} bytes", + key.key.len(), + MIN_OCT_LEN_BYTES + ))); + } + Ok(self) + } + } + } +} + +fn parse_pem(pem: &str) -> Result { + pem::parse(pem).map_err(|_| KeyError::InvalidPem) +} + +fn handle_oct_key(key: &Pem) -> Result { + let key = key.contents().to_vec().into(); + Ok(BarePrivateKeyInner::HS256(HmacKey { key })) +} + +fn handle_ec_key(key: &[u8]) -> Result { + let mut reader = SliceReader::new(key).map_err(|_| KeyError::DecodeError)?; + let decoded_key = EcPrivateKey::decode(&mut reader).map_err(|_| KeyError::DecodeError)?; + + if let Some(parameters) = decoded_key.parameters { + if parameters.named_curve() == Some(SECP_256_R_1) { + let key = p256::SecretKey::from_slice(decoded_key.private_key) + .map_err(|_| KeyError::DecodeError)?; + return Ok(BarePrivateKeyInner::ES256(key)); + } + } + + Err(KeyError::InvalidEcParameters) +} + +fn handle_rsa_key(key: &Pem) -> Result { + let mut reader = SliceReader::new(key.contents()).map_err(|_| KeyError::DecodeError)?; + let _decoded_key = + pkcs1::RsaPrivateKey::decode(&mut reader).map_err(|_| KeyError::DecodeError)?; + + Ok(BarePrivateKeyInner::RS256(PrivatePkcs1KeyDer::from( + key.contents().to_vec(), + ))) +} + +fn handle_pkcs8_key(key: &[u8]) -> Result { + let mut reader = SliceReader::new(key).map_err(|_| KeyError::DecodeError)?; + let decoded_key = PrivateKeyInfo::decode(&mut reader).map_err(|_| KeyError::DecodeError)?; + + match decoded_key.algorithm.oid { + ID_EC_PUBLIC_KEY => { + // Ensure the curve is P-256 + if decoded_key.algorithm.parameters_oid() != Ok(SECP_256_R_1) { + return Err(KeyError::InvalidEcParameters); + } + let mut reader = + SliceReader::new(decoded_key.private_key).map_err(|_| KeyError::DecodeError)?; + let key = EcPrivateKey::decode(&mut reader).map_err(|_| KeyError::DecodeError)?; + let key = + p256::SecretKey::from_slice(key.private_key).map_err(|_| KeyError::DecodeError)?; + Ok(BarePrivateKeyInner::ES256(key)) + } + RSA_ENCRYPTION => { + RsaKeyPair::from_der(decoded_key.private_key) + .map_err(|e| KeyError::KeyValidationError(KeyValidationError(e.to_string())))?; + Ok(BarePrivateKeyInner::RS256(PrivatePkcs1KeyDer::from( + decoded_key.private_key.to_vec(), + ))) + } + _ => Err(KeyError::UnsupportedKeyType( + decoded_key.algorithm.oid.to_string(), + )), + } +} + +fn pkcs8_from_ec(key: &p256::SecretKey) -> Result, KeyError> { + let key_bytes = key.to_bytes(); + let public_key_bytes = key.public_key().to_sec1_bytes().into_vec(); + let mut vec = Vec::new(); + EcPrivateKey { + private_key: key_bytes.as_ref(), + parameters: Some(EcParameters::NamedCurve(SECP_256_R_1)), + public_key: Some(public_key_bytes.as_ref()), + } + .encode_to_vec(&mut vec) + .map_err(|_| KeyError::EncodeError)?; + + let pkcs8 = pkcs8::PrivateKeyInfo { + algorithm: AlgorithmIdentifier { + oid: ID_EC_PUBLIC_KEY, + parameters: Some(AnyRef::from(&EcParameters::NamedCurve(SECP_256_R_1))), + }, + private_key: &vec, + public_key: None, + }; + let mut buf = Vec::new(); + pkcs8 + .encode_to_vec(&mut buf) + .map_err(|_| KeyError::EncodeError)?; + Ok(buf) +} + +impl TryInto for &BarePrivateKey { + type Error = KeyError; + + fn try_into(self) -> Result { + match &self.inner { + BarePrivateKeyInner::RS256(key) => Ok(jsonwebtoken::EncodingKey::from_rsa_der( + key.secret_pkcs1_der(), + )), + BarePrivateKeyInner::ES256(key) => { + Ok(jsonwebtoken::EncodingKey::from_ec_der(&pkcs8_from_ec(key)?)) + } + BarePrivateKeyInner::HS256(key) => Ok(jsonwebtoken::EncodingKey::from_secret(&key.key)), + } + } +} + +impl TryInto for &BarePublicKey { + type Error = KeyError; + + fn try_into(self) -> Result { + match &self.inner { + BarePublicKeyInner::RS256 { n, e } => { + Ok(jsonwebtoken::DecodingKey::from_rsa_raw_components( + &n.to_bytes_be(), + &e.to_bytes_be(), + )) + } + BarePublicKeyInner::ES256(key) => { + Ok(jsonwebtoken::DecodingKey::from_ec_der(&key.to_sec1_bytes())) + } + BarePublicKeyInner::HS256(key) => Ok(jsonwebtoken::DecodingKey::from_secret(&key.key)), + } + } +} + +impl TryFrom<&BarePrivateKeyInner> for BarePublicKeyInner { + type Error = KeyError; + + fn try_from(key: &BarePrivateKeyInner) -> Result { + match key { + BarePrivateKeyInner::RS256(key) => { + let rsa = pkcs1::RsaPrivateKey::from_der(key.secret_pkcs1_der()) + .map_err(|_| KeyError::DecodeError)?; + let n = BigUint::from_bytes_be(rsa.modulus.as_bytes()); + let e = BigUint::from_bytes_be(rsa.public_exponent.as_bytes()); + Ok(BarePublicKeyInner::RS256 { n, e }) + } + BarePrivateKeyInner::ES256(key) => { + let pk = key.public_key(); + Ok(BarePublicKeyInner::ES256(pk)) + } + BarePrivateKeyInner::HS256(key) => Ok(BarePublicKeyInner::HS256(key.clone())), + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct BarePublicKey { + pub(crate) inner: BarePublicKeyInner, +} + +impl std::fmt::Debug for BarePublicKeyInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + BarePublicKeyInner::RS256 { n, e } => write!(f, "RS256({n}, {e})"), + BarePublicKeyInner::ES256(pk) => write!(f, "ES256({pk:?})"), + BarePublicKeyInner::HS256(_key) => write!(f, "HS256(...)"), + } + } +} + +impl std::hash::Hash for BarePublicKeyInner { + fn hash(&self, state: &mut H) { + match &self { + BarePublicKeyInner::RS256 { n, e } => { + n.hash(state); + e.hash(state); + } + BarePublicKeyInner::ES256(pk) => { + pk.to_encoded_point(false).hash(state); + } + BarePublicKeyInner::HS256(key) => { + key.hash(state); + } + } + } +} + +impl Eq for BarePublicKeyInner {} + +impl PartialEq for BarePublicKeyInner { + fn eq(&self, other: &Self) -> bool { + match (&self, &other) { + ( + BarePublicKeyInner::RS256 { n: n1, e: e1 }, + BarePublicKeyInner::RS256 { n: n2, e: e2 }, + ) => n1 == n2 && e1 == e2, + (BarePublicKeyInner::ES256(pk1), BarePublicKeyInner::ES256(pk2)) => pk1 == pk2, + (BarePublicKeyInner::HS256(key1), BarePublicKeyInner::HS256(key2)) => key1 == key2, + _ => false, + } + } +} + +#[derive(Clone)] +pub(crate) enum BarePublicKeyInner { + RS256 { n: BigUint, e: BigUint }, + ES256(p256::PublicKey), + HS256(HmacKey), +} + +impl BarePublicKey { + fn from_unvalidated(inner: BarePublicKeyInner) -> Result { + Ok(Self { + inner: inner.validate()?, + }) + } + + /// Load an ECDSA public key from a JWK. + pub fn from_jwt_ec(crv: &str, x: &str, y: &str) -> Result { + if crv != "P-256" { + return Err(KeyError::UnsupportedKeyType(format!( + "EC curve ({}) not supported", + crv + ))); + } + + let x = b64_decode(x)?; + let y = b64_decode(y)?; + let x = GenericArray::::from_slice(x.as_slice()); + let y = GenericArray::::from_slice(y.as_slice()); + let point = p256::EncodedPoint::from_affine_coordinates(x, y, false); + let key = p256::PublicKey::from_encoded_point(&point) + .into_option() + .ok_or(KeyError::DecodeError)?; + Self::from_unvalidated(BarePublicKeyInner::ES256(key)) + } + + /// Load an RSA public key from a JWK. + pub fn from_jwt_rsa(n: &str, e: &str) -> Result { + let n = BigUint::from_bytes_be(&b64_decode(n)?); + let e = BigUint::from_bytes_be(&b64_decode(e)?); + Self::from_unvalidated(BarePublicKeyInner::RS256 { n, e }) + } + + pub fn from_jwt_oct(k: &str) -> Result { + let key = b64_decode(k)?; + Self::from_unvalidated(BarePublicKeyInner::HS256(HmacKey { key })) + } + + /// Creates a `BarePublicKey` from a PEM-encoded public or private key. If the + /// PEM-encoded file contains a private key, it will be converted to a public key + /// and the private key data will be discarded. + /// + /// Supported formats include the private key formats from [`BareKey::from_pem`], + /// `SPKI`-containers (`PUBLIC KEY` and `EC PUBLIC KEY`), and `RSA PUBLIC KEY` + /// traditional-style keys (`RsaPublicKey`). + pub fn from_pem(pem: &str) -> Result { + let key = BareKey::from_pem(pem)?; + key.try_to_public() + } + + pub fn from_pem_multiple(pem: &str) -> Result>, KeyError> { + Ok(BareKey::from_pem_multiple(pem)? + .into_iter() + .map(|key| key.and_then(|k| k.try_to_public())) + .collect()) + } + + pub fn clone_key(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + + pub fn key_type(&self) -> KeyType { + self.inner.key_type() + } + + pub fn to_pem(&self) -> String { + self.inner.to_pem() + } +} + +impl BarePublicKeyInner { + pub fn key_type(&self) -> KeyType { + match &self { + BarePublicKeyInner::RS256 { .. } => KeyType::RS256, + BarePublicKeyInner::ES256(..) => KeyType::ES256, + BarePublicKeyInner::HS256(..) => KeyType::HS256, + } + } + + pub fn to_pem(&self) -> String { + // We use unwrap() here but these cases should not be reachable + match &self { + BarePublicKeyInner::RS256 { n, e } => { + let mut v = Vec::new(); + pkcs1::RsaPublicKey { + modulus: UintRef::new(&n.to_bytes_be()).unwrap(), + public_exponent: UintRef::new(&e.to_bytes_be()).unwrap(), + } + .encode_to_vec(&mut v) + .unwrap(); + pem::encode(&Pem::new("RSA PUBLIC KEY", v)) + } + BarePublicKeyInner::ES256(spki) => { + let spki = SubjectPublicKeyInfoOwned { + algorithm: AlgorithmIdentifier { + oid: ID_EC_PUBLIC_KEY, + parameters: Some( + AnyRef::from(&EcParameters::NamedCurve(SECP_256_R_1)).into(), + ), + }, + subject_public_key: BitString::from_bytes(&spki.to_sec1_bytes()).unwrap(), + }; + let mut v = vec![]; + spki.encode_to_vec(&mut v).unwrap(); + pem::encode(&Pem::new("PUBLIC KEY", v)) + } + BarePublicKeyInner::HS256(key) => { + pem::encode(&Pem::new("JWT OCTAL KEY", key.key.as_slice())) + } + } + } + + fn validate(self) -> Result { + match &self { + BarePublicKeyInner::RS256 { n, e } => validate_rsa_pubkey(n, e), + BarePublicKeyInner::ES256(pk) => validate_ecdsa_pubkey(pk), + BarePublicKeyInner::HS256(key) => { + if key.key.len() < MIN_OCT_LEN_BYTES { + return Err(KeyError::UnsupportedKeyType(format!( + "oct key ({} bytes) < {} bytes", + key.key.len(), + MIN_OCT_LEN_BYTES + ))); + } + Ok(()) + } + }?; + Ok(self) + } +} + +fn handle_spki_pubkey(key: &Pem) -> Result { + let mut reader = SliceReader::new(key.contents()).map_err(|_| KeyError::DecodeError)?; + let decoded_key = pkcs8::SubjectPublicKeyInfo::::decode(&mut reader) + .map_err(|_| KeyError::DecodeError)?; + + match decoded_key.algorithm.oid { + ID_EC_PUBLIC_KEY => { + let pk = p256::PublicKey::from_sec1_bytes(decoded_key.subject_public_key.raw_bytes()) + .map_err(|_| KeyError::DecodeError)?; + Ok(BarePublicKeyInner::ES256(pk)) + } + RSA_ENCRYPTION => { + let pub_key = pkcs1::RsaPublicKey::from_der(decoded_key.subject_public_key.raw_bytes()) + .map_err(|_| KeyError::DecodeError)?; + Ok(BarePublicKeyInner::RS256 { + n: BigUint::from_bytes_be(pub_key.modulus.as_bytes()), + e: BigUint::from_bytes_be(pub_key.public_exponent.as_bytes()), + }) + } + _ => Err(KeyError::UnsupportedKeyType( + decoded_key.algorithm.oid.to_string(), + )), + } +} + +fn handle_rsa_pubkey(key: &Pem) -> Result { + let mut reader = SliceReader::new(key.contents()).map_err(|_| KeyError::DecodeError)?; + let decoded_key = + pkcs1::RsaPublicKey::decode(&mut reader).map_err(|_| KeyError::DecodeError)?; + Ok(BarePublicKeyInner::RS256 { + n: BigUint::from_bytes_be(decoded_key.modulus.as_bytes()), + e: BigUint::from_bytes_be(decoded_key.public_exponent.as_bytes()), + }) +} + +/// Decode a base64 string with optional padding, since jwcrypto also seems to +/// accept this. +/// +/// :JWKs make use of the base64url encoding as defined in RFC 4648 [RFC4648]. +/// As allowed by Section 3.2 of the RFC, this specification mandates that +/// base64url encoding when used with JWKs MUST NOT use padding. Notes on +/// implementing base64url encoding can be found in the JWS [JWS] +/// specification."" +fn b64_decode(s: &str) -> Result>, KeyError> { + let vec = if s.ends_with('=') { + base64ct::Base64Url::decode_vec(s).map_err(|_| KeyError::DecodeError)? + } else { + base64ct::Base64UrlUnpadded::decode_vec(s).map_err(|_| KeyError::DecodeError)? + }; + Ok(zeroize::Zeroizing::new(vec)) +} + +fn validate_ecdsa_key_pair(key: &p256::SecretKey) -> Result<(), KeyError> { + let pkcs8_bytes = pkcs8_from_ec(key)?; + let _keypair = ring::signature::EcdsaKeyPair::from_pkcs8( + &ECDSA_P256_SHA256_FIXED_SIGNING, + &pkcs8_bytes, + &SystemRandom::new(), + ) + .map_err(|e| KeyError::KeyValidationError(KeyValidationError(e.to_string())))?; + Ok(()) +} + +fn validate_rsa_key_pair(pkcs8: &[u8]) -> Result<(), KeyError> { + let _keypair = ring::signature::RsaKeyPair::from_der(pkcs8) + .map_err(|e| KeyError::KeyValidationError(KeyValidationError(e.to_string())))?; + Ok(()) +} + +fn validate_rsa_pubkey(n: &BigUint, e: &BigUint) -> Result<(), KeyError> { + // TODO: Should we validate more than this? + if e == &BigUint::from(3_u8) { + return Err(KeyError::UnsupportedKeyType("RSA e=3".to_string())); + } + if n.bits() < MIN_RSA_KEY_BITS { + return Err(KeyError::UnsupportedKeyType(format!( + "RSA n ({}) < {} bits", + n.bits(), + MIN_RSA_KEY_BITS + ))); + } + Ok(()) +} + +fn validate_ecdsa_pubkey(_pk: &p256::PublicKey) -> Result<(), KeyError> { + // TODO: Should we validate more than this? + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::hash::{Hash, Hasher}; + + use super::*; + use rstest::*; + + #[test] + fn test_fallback_rsa_keygen() { + let rsa = optional_openssl_rsa_keygen(DEFAULT_GEN_RSA_KEY_BITS); + if let Some(rsa) = rsa { + println!("{}", rsa.to_pem()); + } else { + println!("Failed to generate RSA key"); + } + } + + fn load_test_file(filename: &str) -> String { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src/testcases") + .join(filename); + eprintln!("FILE: {}", path.display()); + std::fs::read_to_string(path).unwrap() + } + + #[rstest] + #[case::ec_pk8("prime256v1-prv-pkcs8.pem")] + #[case::ec_sec1("prime256v1-prv-sec1.pem")] + #[case::rsa_pkcs1("rsa2048-prv-pkcs1.pem")] + #[case::rsa_pkcs8("rsa2048-prv-pkcs8.pem")] + fn test_from_pem_private(#[case] pem: &str) { + let input = load_test_file(pem); + eprintln!("IN:\n{input}"); + let key = BarePrivateKey::from_pem(&input).unwrap(); + eprintln!("OUT:\n{}", key.to_pem()); + let key = BarePrivateKey::from_pem(&key.to_pem()).expect("Failed to round-trip"); + + let key_type = key.key_type(); + let encoding_key = (&key).try_into().unwrap(); + let token = match key_type { + KeyType::RS256 => jsonwebtoken::encode( + &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256), + &["claim"], + &encoding_key, + ) + .unwrap(), + KeyType::ES256 => jsonwebtoken::encode( + &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::ES256), + &["claim"], + &encoding_key, + ) + .unwrap(), + _ => unreachable!(), + }; + println!("{}", token); + } + + #[rstest] + #[case::ec_pk8("prime256v1-prv-pkcs8.pem")] + #[case::ec_sec1("prime256v1-prv-sec1.pem")] + #[case::ec_spki_unc("prime256v1-pub-spki-uncompressed.pem")] + #[case::ec_spki("prime256v1-pub-spki.pem")] + fn test_from_pem_public_ec(#[case] pem: &str) { + let key = BarePublicKey::from_pem(&load_test_file(pem)).unwrap(); + println!("{}", key.to_pem()); + BarePublicKey::from_pem(&key.to_pem()).expect("Failed to round-trip"); + } + + #[rstest] + #[case::rsa_pkcs1("rsa2048-prv-pkcs1.pem")] + #[case::rsa_pkcs8("rsa2048-prv-pkcs8.pem")] + #[case::rsa_spki("rsa2048-pub-pkcs1.pem")] + #[case::rsa_spki_pkcs8("rsa2048-pub-pkcs8.pem")] + fn test_from_pem_public_rsa(#[case] pem: &str) { + let key = BarePublicKey::from_pem(&load_test_file(pem)).unwrap(); + println!("{}", key.to_pem()); + BarePublicKey::from_pem(&key.to_pem()).expect("Failed to round-trip"); + } + + /// Test that the equality and hash functions work for BarePublicKey and BareKey. All + /// key forms should be equal. + #[test] + fn test_eq_hash() { + let key1 = BarePrivateKey::from_pem(&load_test_file("rsa2048-prv-pkcs1.pem")).unwrap(); + + for key in [ + "rsa2048-prv-pkcs1.pem", + "rsa2048-prv-pkcs8.pem", + "rsa2048-pub-pkcs1.pem", + "rsa2048-pub-pkcs8.pem", + ] { + if key.contains("pub") { + let key1: BarePublicKey = key1.to_public().unwrap(); + let key2 = BarePublicKey::from_pem(&load_test_file(key)).unwrap(); + assert_eq!(key1, key2); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + key1.hash(&mut hasher); + let hash1 = hasher.finish(); + hasher = std::collections::hash_map::DefaultHasher::new(); + key2.hash(&mut hasher); + let hash2 = hasher.finish(); + assert_eq!(hash1, hash2); + } else { + let key2 = BarePrivateKey::from_pem(&load_test_file(key)).unwrap(); + assert_eq!(key1, key2); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + key1.hash(&mut hasher); + let hash1 = hasher.finish(); + hasher = std::collections::hash_map::DefaultHasher::new(); + key2.hash(&mut hasher); + let hash2 = hasher.finish(); + assert_eq!(hash1, hash2); + } + } + } + + #[test] + fn test_jwt_ec_key() { + let key = BarePrivateKey::from_jwt_ec( + "P-256", + "w0pL1NOlKBOMtSOvUf6aFeEguWFCclQjWrWqHtHdEA8", + "ZX_Ajm_22hdQbXImmtmaG-9TQ2z5Dt5Hbia0JzibvXc", + "9r0Do-XFPyMYM6XCtOAT8AgY2xyRYLuS4U-_xXHDjeE", + ) + .unwrap(); + println!("{}", key.to_pem()); + } + + #[test] + fn test_jwt_rsa_key() { + let e = "AQAB"; + let n = r#"oW-OMq9ATezmeSGLlTbp--Epar64s7qZSi2hTgmdmlaJdpDO8X_EunUIB4DLyPEsOH45-W + P2xxmw9Uv0UHfvfHsqOKx6vyLjSkDcrUddBWLWhJ5vVm2iHW8FGtYmaLWcHyyh2QiVQUriUNo3HtQqGRKBw9V2X + gIJ4tzIysuxiMM0uFs8IAvl6TX7MHgUnW4rohyDCJiWLs8UDHpdN3mBpIiokrRr_iTTWNb5m_HKWGJ7RBsLaRsX + VhxgxZm2PrEEcgb5XlcBbRqOD-5LilCGw5IcX4y12vl_zGpdn-X63UjZmgjRyXKNLh7pOMyKDvWl5vp89w-DKTV + 5oN6CkVnI5w"# + .replace(char::is_whitespace, ""); + let d = r#"QkfWhrnMeZIP6GDc-dUTiV5fTlvi4qv0vu9wIGWzRwhLpRn8VUwDnhhpxQbc5HIcmU8-B0 + ZDLmi-bmASfa1Ybu_0nFM4jFxLHJP35s77grgbYlTYWpBltJb97hBJsckKwgPlqYGsIiQYOmD1q5spc6TVEW4Fj + MBihbnnWNf72q2_1CeYgBmLxaMDukUJ8gAaRXkGT0_4YBVBioPUpt_JrfX4dvtJlV3ehXnjN2KiH0xxXHinYdQr + NSjrUSMUFRCNvSadmuYp1Aoxgsa43VoNAQqbvDRzxjX8eqjdXykVU_ILLwveH9NpZVho727Vd2ISvhwjtjDYMLY + q6H_Rj6yrTQ"# + .replace(char::is_whitespace, ""); + let p = r#"1Ce5utgQeHjSPQ_WbUzNt2wRCN8_VbH2LcmPzvxx1XfP7N8FpPs7isx5RpGnrAcVlxq9bI + MgKq5wtEW2mK4rHB9n9kIxQwDGD7YGOSU3uK-Mi_ygm7ytTo3keMQ9Vj_W05UCT4l8RHvHwU6h-hvCIcN0TnHO0 + mX4JsAgRB-XmuU"# + .replace(char::is_whitespace, ""); + let q = r#"wsx4ar__O_4dAva_emh7nOSAarF0UBrCuckHImCHwCM62mntXXhjAyY7t9BMQ4ccgYLNeW + 1l9lKpP3orkpYY1wsRMWGrQyDZlKqwNp-x5IG7c5RescuCJ4Yy5JO_PmtXOwukWH7YUTk7nWCCYNCxfHCsxvr-X + T4oct9FZAtHu9s"# + .replace(char::is_whitespace, ""); + let key = BarePrivateKey::from_jwt_rsa(&n, e, &d, &p, &q).unwrap(); + println!("{}", key.to_pem()); + } + + #[test] + fn test_hs256_key_generation() { + let key = BarePrivateKey::generate(KeyType::HS256).unwrap(); + let pem = key.to_pem(); + println!("{}", pem); + } + + #[test] + fn test_es256_key_generation() { + let key = BarePrivateKey::generate(KeyType::ES256).unwrap(); + let pem = key.to_pem(); + println!("{}", pem); + let key2 = BarePrivateKey::from_pem(&pem).expect("Failed to round-trip"); + println!("{}", key2.to_pem()); + } + + #[test] + fn test_rs256_key_generation() { + let key = BarePrivateKey::generate(KeyType::RS256).unwrap(); + let pem = key.to_pem(); + println!("{}", pem); + let key2 = BarePrivateKey::from_pem(&pem).expect("Failed to round-trip"); + println!("{}", key2.to_pem()); + } + + #[test] + fn test_deserialize_private_keys() { + let json = load_test_file("jwkset-prv.json"); + let keys: SerializedKeys = serde_json::from_str(&json).unwrap(); + println!("{:?}", keys); + + println!("{}", serde_json::to_string(&keys).unwrap()); + } + + #[test] + fn test_deserialize_public_keys() { + let json = load_test_file("jwkset-pub.json"); + let keys: SerializedKeys = serde_json::from_str(&json).unwrap(); + println!("{:?}", keys); + println!("{}", serde_json::to_string(&keys).unwrap()); + } +} diff --git a/rust/gel-jwt/src/key.rs b/rust/gel-jwt/src/key.rs new file mode 100644 index 000000000000..7b5c8c885f60 --- /dev/null +++ b/rust/gel-jwt/src/key.rs @@ -0,0 +1,612 @@ +use jsonwebtoken::{Algorithm, Header, Validation}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{HashMap, HashSet}, + fmt::Debug, + sync::Arc, + time::Duration, +}; + +use crate::{ + bare_key::{BareKeyInner, SerializedKey}, + registry::IsKey, + Any, BareKey, BarePrivateKey, BarePublicKey, KeyError, OpaqueValidationFailureReason, + SignatureError, ValidationError, +}; + +#[derive(Clone, Copy, Debug, derive_more::Display, PartialEq, Eq)] +pub enum KeyType { + RS256, + ES256, + HS256, +} + +#[derive(Clone, Serialize, Deserialize, Default)] +pub struct SigningContext { + pub expiry: Option, + pub issuer: Option, + pub audience: Option, + pub allow: HashMap>, + pub deny: HashMap>, + pub not_before: Option, +} + +#[derive(Serialize, Deserialize)] +struct Token { + #[serde(rename = "exp", default, skip_serializing_if = "Option::is_none")] + pub expiry: Option, + #[serde(rename = "iss", default, skip_serializing_if = "Option::is_none")] + pub issuer: Option, + #[serde(rename = "aud", default, skip_serializing_if = "Option::is_none")] + pub audience: Option, + #[serde(rename = "iat", default, skip_serializing_if = "Option::is_none")] + pub issued_at: Option, + #[serde(rename = "nbf", default, skip_serializing_if = "Option::is_none")] + pub not_before: Option, + #[serde(flatten)] + claims: HashMap, +} + +/// A private key with key-signing capabilities. +pub struct PrivateKey { + pub(crate) kid: Option, + pub(crate) inner: Arc, +} + +impl PrivateKey { + pub fn key_type(&self) -> KeyType { + self.inner.bare_key.key_type() + } + + pub fn set_kid(&mut self, kid: Option) { + self.kid = kid; + } + + pub fn from_bare_private_key( + kid: Option, + key: BarePrivateKey, + ) -> Result { + let encoding_key = (&key).try_into()?; + let decoding_key = (&key.to_public()?).try_into()?; + let inner = PrivateKeyInner { + bare_key: key, + encoding_key, + decoding_key, + } + .into(); + Ok(Self { kid, inner }) + } + + pub fn generate(kid: Option, kty: KeyType) -> Result { + let key = BarePrivateKey::generate(kty)?; + Self::from_bare_private_key(kid, key) + } + + pub fn clone_key(&self) -> Self { + Self { + kid: self.kid.clone(), + inner: self.inner.clone(), + } + } + + pub fn sign( + &self, + claims: HashMap, + ctx: &SigningContext, + ) -> Result { + sign_token( + self.key_type(), + &self.inner.encoding_key, + self.kid.as_deref(), + claims, + ctx, + ) + } + + pub fn validate( + &self, + token: &str, + ctx: &SigningContext, + ) -> Result, ValidationError> { + validate_token( + self.key_type(), + &self.inner.decoding_key, + self.kid.as_deref(), + token, + ctx, + ) + } +} + +impl IsKey for PrivateKey { + type Inner = Arc; + + fn key_type(inner: &Self::Inner) -> KeyType { + inner.bare_key.key_type() + } + + fn inner(&self) -> &Self::Inner { + &self.inner + } + + fn from_inner(kid: Option, inner: Self::Inner) -> Self { + PrivateKey { kid, inner } + } + + fn into_inner(self) -> (Option, Self::Inner) { + (self.kid, self.inner) + } + + fn get_serialized_key(key: SerializedKey) -> Option { + match key { + SerializedKey::Private(kid, key) => { + Some(PrivateKey::from_bare_private_key(kid, key).ok()?) + } + _ => None, + } + } + + fn to_serialized_key(kid: Option<&str>, key: &Self::Inner) -> SerializedKey { + SerializedKey::Private(kid.map(String::from), key.bare_key.clone_key()) + } + + fn from_pem(pem: &str) -> Result>, KeyError> { + BarePrivateKey::from_pem_multiple(pem).map(|keys| { + keys.into_iter() + .map(|k| k.and_then(|bare_key| PrivateKey::from_bare_private_key(None, bare_key))) + .collect() + }) + } + + fn to_pem(inner: &Self::Inner) -> String { + inner.bare_key.to_pem() + } + + fn decoding_key(inner: &Self::Inner) -> &jsonwebtoken::DecodingKey { + &inner.decoding_key + } + + fn encoding_key(inner: &Self::Inner) -> Option<&jsonwebtoken::EncodingKey> { + Some(&inner.encoding_key) + } +} + +pub(crate) fn sign_token( + key_type: KeyType, + encoding_key: &jsonwebtoken::EncodingKey, + kid: Option<&str>, + claims: HashMap, + ctx: &SigningContext, +) -> Result { + let mut header = Header { + kid: kid.map(String::from), + ..Default::default() + }; + match key_type { + KeyType::HS256 => {} + KeyType::ES256 => header.alg = jsonwebtoken::Algorithm::ES256, + KeyType::RS256 => header.alg = jsonwebtoken::Algorithm::RS256, + } + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as usize; + + let (issued_at, not_before) = if let Some(not_before) = ctx.not_before { + ( + Some(now), + Some(now.saturating_sub(not_before.as_secs() as usize)), + ) + } else { + (None, None) + }; + + let token = Token { + expiry: ctx.expiry.map(|d| now.saturating_add(d.as_secs() as _)), + issuer: ctx.issuer.clone(), + audience: ctx.audience.clone(), + issued_at, + not_before, + claims, + }; + + jsonwebtoken::encode(&header, &token, encoding_key) + .map_err(|e| SignatureError::SignatureError(e.to_string())) +} + +/// Returns the raw claims from the token, including those we may have added +/// as part of the signature process. +pub(crate) fn validate_token( + key_type: KeyType, + decoding_key: &jsonwebtoken::DecodingKey, + kid: Option<&str>, + token: &str, + ctx: &SigningContext, +) -> Result, ValidationError> { + let mut validation = Validation::new(match key_type { + KeyType::ES256 => Algorithm::ES256, + KeyType::HS256 => Algorithm::HS256, + KeyType::RS256 => Algorithm::RS256, + }); + + if ctx.expiry.is_none() { + validation.required_spec_claims.remove("exp"); + } + if ctx.not_before.is_none() { + validation.required_spec_claims.remove("nbf"); + } + if let Some(aud) = &ctx.audience { + validation.set_audience(&[aud]); + } + if let Some(iss) = &ctx.issuer { + validation.set_issuer(&[iss]); + } + + let token = jsonwebtoken::decode::>(token, decoding_key, &validation) + .map_err(|e| match e.kind() { + jsonwebtoken::errors::ErrorKind::InvalidSignature => { + OpaqueValidationFailureReason::InvalidSignature + } + _ => OpaqueValidationFailureReason::Failure(format!("{:?}", e.kind())), + })?; + + if let (Some(token_kid), Some(expected_kid)) = (token.header.kid, kid) { + if token_kid != expected_kid { + return Err(OpaqueValidationFailureReason::InvalidHeader( + "kid".to_string(), + token_kid, + Some(expected_kid.to_string()), + ) + .into()); + } + } + + for (claim, values) in &ctx.allow { + let value = token.claims.get(claim); + match value { + Some(Any::String(value)) => { + if !values.contains(value.as_ref()) { + return Err(OpaqueValidationFailureReason::InvalidClaimValue( + claim.to_string(), + Some(value.to_string()), + ) + .into()); + } + } + _ => { + return Err(OpaqueValidationFailureReason::InvalidClaimValue( + claim.to_string(), + None, + ) + .into()); + } + } + } + + for (claim, values) in &ctx.deny { + let value = token.claims.get(claim); + match value { + Some(Any::String(value)) => { + if values.contains(value.as_ref()) { + return Err(OpaqueValidationFailureReason::InvalidClaimValue( + claim.to_string(), + Some(value.to_string()), + ) + .into()); + } + } + _ => { + return Err(OpaqueValidationFailureReason::InvalidClaimValue( + claim.to_string(), + None, + ) + .into()); + } + } + } + + // Remove any claims that were validated automatically + let mut claims = token.claims; + if ctx.audience.is_some() { + claims.remove("aud"); + } + if ctx.issuer.is_some() { + claims.remove("iss"); + } + if ctx.expiry.is_some() { + claims.remove("exp"); + } + if ctx.not_before.is_some() { + claims.remove("nbf"); + } + + Ok(claims) +} + +pub(crate) struct PrivateKeyInner { + pub(crate) bare_key: BarePrivateKey, + pub(crate) encoding_key: jsonwebtoken::EncodingKey, + pub(crate) decoding_key: jsonwebtoken::DecodingKey, +} + +impl std::hash::Hash for PrivateKeyInner { + fn hash(&self, state: &mut H) { + self.bare_key.hash(state); + } +} + +impl PartialEq for PrivateKeyInner { + fn eq(&self, other: &Self) -> bool { + self.bare_key == other.bare_key + } +} + +impl std::fmt::Debug for PrivateKeyInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.bare_key.fmt(f) + } +} + +impl Eq for PrivateKeyInner {} + +/// A public key with key-validation capabilities. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PublicKey { + kid: Option, + inner: Arc, +} + +impl PublicKey { + pub fn key_type(&self) -> KeyType { + self.inner.bare_key.key_type() + } + + pub fn set_kid(&mut self, kid: Option) { + self.kid = kid; + } + + pub fn from_bare_public_key(kid: Option, key: BarePublicKey) -> Result { + let decoding_key: jsonwebtoken::DecodingKey = (&key).try_into()?; + let inner = PublicKeyInner { + decoding_key, + bare_key: key, + } + .into(); + Ok(Self { kid, inner }) + } + + pub fn validate( + &self, + token: &str, + ctx: &SigningContext, + ) -> Result, ValidationError> { + validate_token( + self.key_type(), + &self.inner.decoding_key, + self.kid.as_deref(), + token, + ctx, + ) + } +} + +impl IsKey for PublicKey { + type Inner = Arc; + + fn key_type(inner: &Self::Inner) -> KeyType { + inner.bare_key.key_type() + } + + fn inner(&self) -> &Self::Inner { + &self.inner + } + + fn from_inner(kid: Option, inner: Self::Inner) -> Self { + PublicKey { kid, inner } + } + + fn into_inner(self) -> (Option, Self::Inner) { + (self.kid, self.inner) + } + + fn get_serialized_key(key: SerializedKey) -> Option { + match key { + SerializedKey::Private(kid, key) => { + Some(PublicKey::from_bare_public_key(kid, key.to_public().ok()?).ok()?) + } + SerializedKey::Public(kid, key) => { + Some(PublicKey::from_bare_public_key(kid, key).ok()?) + } + _ => None, + } + } + + fn to_serialized_key(kid: Option<&str>, key: &Self::Inner) -> SerializedKey { + SerializedKey::Public(kid.map(String::from), key.bare_key.clone_key()) + } + + fn from_pem(pem: &str) -> Result>, KeyError> { + BarePublicKey::from_pem_multiple(pem).map(|keys| { + keys.into_iter() + .map(|k| k.and_then(|bare_key| PublicKey::from_bare_public_key(None, bare_key))) + .collect() + }) + } + + fn to_pem(inner: &Self::Inner) -> String { + inner.bare_key.to_pem() + } + + fn decoding_key(inner: &Self::Inner) -> &jsonwebtoken::DecodingKey { + &inner.decoding_key + } + + fn encoding_key(_: &Self::Inner) -> Option<&jsonwebtoken::EncodingKey> { + None + } +} + +pub struct PublicKeyInner { + pub(crate) bare_key: BarePublicKey, + pub(crate) decoding_key: jsonwebtoken::DecodingKey, +} + +impl std::fmt::Debug for PublicKeyInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.bare_key.fmt(f) + } +} + +impl std::hash::Hash for PublicKeyInner { + fn hash(&self, state: &mut H) { + self.bare_key.hash(state); + } +} + +impl PartialEq for PublicKeyInner { + fn eq(&self, other: &Self) -> bool { + self.bare_key == other.bare_key + } +} + +impl Eq for PublicKeyInner {} + +/// A key which is either a private or public key. +pub struct Key { + kid: Option, + inner: KeyInner, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum KeyInner { + Private(Arc), + Public(Arc), +} + +impl KeyInner {} + +impl Key { + pub fn key_type(&self) -> KeyType { + match &self.inner { + KeyInner::Private(inner) => inner.bare_key.key_type(), + KeyInner::Public(inner) => inner.bare_key.key_type(), + } + } + + pub fn from_bare_key(kid: Option, key: BareKey) -> Result { + Ok(match key.inner { + BareKeyInner::Private(inner) => { + PrivateKey::from_bare_private_key(kid, BarePrivateKey { inner })?.into() + } + BareKeyInner::Public(inner) => { + PublicKey::from_bare_public_key(kid, BarePublicKey { inner })?.into() + } + }) + } + + pub fn from_bare_private_key( + kid: Option, + key: BarePrivateKey, + ) -> Result { + Ok(PrivateKey::from_bare_private_key(kid, key)?.into()) + } + + pub fn from_bare_public_key(kid: Option, key: BarePublicKey) -> Result { + Ok(PublicKey::from_bare_public_key(kid, key)?.into()) + } +} + +impl From for Key { + fn from(key: PrivateKey) -> Self { + Key { + kid: key.kid, + inner: KeyInner::Private(key.inner), + } + } +} + +impl From for Key { + fn from(key: PublicKey) -> Self { + Key { + kid: key.kid, + inner: KeyInner::Public(key.inner), + } + } +} + +impl IsKey for Key { + type Inner = KeyInner; + + fn key_type(inner: &Self::Inner) -> KeyType { + match inner { + KeyInner::Private(inner) => inner.bare_key.key_type(), + KeyInner::Public(inner) => inner.bare_key.key_type(), + } + } + + fn inner(&self) -> &Self::Inner { + &self.inner + } + + fn from_inner(kid: Option, inner: Self::Inner) -> Self { + Key { kid, inner } + } + + fn into_inner(self) -> (Option, Self::Inner) { + (self.kid, self.inner) + } + + fn get_serialized_key(key: SerializedKey) -> Option { + match key { + SerializedKey::Private(kid, key) => { + Some(PrivateKey::from_bare_private_key(kid, key).ok()?.into()) + } + SerializedKey::Public(kid, key) => { + Some(PublicKey::from_bare_public_key(kid, key).ok()?.into()) + } + _ => None, + } + } + + fn to_serialized_key(kid: Option<&str>, key: &Self::Inner) -> SerializedKey { + match key { + KeyInner::Private(inner) => { + SerializedKey::Private(kid.map(String::from), inner.bare_key.clone_key()) + } + KeyInner::Public(inner) => { + SerializedKey::Public(kid.map(String::from), inner.bare_key.clone_key()) + } + } + } + + fn from_pem(pem: &str) -> Result>, KeyError> { + let keys = BareKey::from_pem_multiple(pem)?; + let mut results = Vec::new(); + for key in keys { + results.push(key.and_then(|key| Self::from_bare_key(None, key))); + } + Ok(results) + } + + fn to_pem(inner: &Self::Inner) -> String { + match inner { + KeyInner::Private(inner) => inner.bare_key.to_pem(), + KeyInner::Public(inner) => inner.bare_key.to_pem(), + } + } + + fn decoding_key(inner: &Self::Inner) -> &jsonwebtoken::DecodingKey { + match inner { + KeyInner::Private(inner) => &inner.decoding_key, + KeyInner::Public(inner) => &inner.decoding_key, + } + } + + fn encoding_key(inner: &Self::Inner) -> Option<&jsonwebtoken::EncodingKey> { + match inner { + KeyInner::Private(inner) => Some(&inner.encoding_key), + KeyInner::Public(_) => None, + } + } +} diff --git a/rust/gel-jwt/src/lib.rs b/rust/gel-jwt/src/lib.rs new file mode 100644 index 000000000000..5cfda4d93b83 --- /dev/null +++ b/rust/gel-jwt/src/lib.rs @@ -0,0 +1,498 @@ +#[cfg(feature = "python_extension")] +pub mod python; + +use std::{borrow::Cow, collections::HashMap, fmt::Debug}; +use thiserror::Error; + +mod bare_key; +mod key; +mod registry; + +pub use bare_key::{BareKey, BarePrivateKey, BarePublicKey}; +pub use key::{Key, KeyType, PrivateKey, PublicKey, SigningContext}; +pub use registry::KeyRegistry; + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum ValidationError { + /// The token format or signature was invalid + #[error("Invalid token")] + Invalid(OpaqueValidationFailureReason), + /// The key is invalid + #[error(transparent)] + KeyError(#[from] KeyError), +} + +impl ValidationError { + /// Display an error not intended for the end-user as it may leak information about the keys + /// and/or tokens. + pub fn error_string_not_for_user(&self) -> String { + match self { + ValidationError::Invalid(OpaqueValidationFailureReason::Failure(s)) => { + format!("Invalid token: {}", s) + } + ValidationError::Invalid(OpaqueValidationFailureReason::InvalidClaimValue( + claim, + value, + )) => format!("Invalid claim value for {claim}: {value:?}"), + ValidationError::Invalid(OpaqueValidationFailureReason::InvalidHeader( + header, + value, + expected, + )) => format!("Invalid header {header}: {value:?}, expected {expected:?}"), + ValidationError::Invalid(OpaqueValidationFailureReason::NoAppropriateKey) => { + "No appropriate key found".to_string() + } + ValidationError::Invalid(OpaqueValidationFailureReason::InvalidSignature) => { + "Invalid signature".to_string() + } + ValidationError::KeyError(error) => format!("Key error: {}", error), + } + } +} + +/// A reason for validation failure that is opaque to debugging or printing to avoid +/// leaking information about the token failure. +#[derive(Eq, PartialEq)] +pub enum OpaqueValidationFailureReason { + NoAppropriateKey, + InvalidSignature, + InvalidClaimValue(String, Option), + InvalidHeader(String, String, Option), + Failure(String), +} + +impl From for ValidationError { + fn from(reason: OpaqueValidationFailureReason) -> Self { + ValidationError::Invalid(reason) + } +} + +impl std::fmt::Debug for OpaqueValidationFailureReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "...") + } +} + +#[derive(Error, Debug)] +pub enum SignatureError { + /// The token format or signature was invalid + #[error("Signature operation failed: {0}")] + SignatureError(String), + /// No appropriate key was found + #[error("No appropriate signing key found")] + NoAppropriateKey, + /// The key is invalid + #[error(transparent)] + KeyError(#[from] KeyError), +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum KeyError { + #[error("Invalid PEM format")] + InvalidPem, + #[error("Invalid JSON format")] + InvalidJson, + #[error("Unsupported key type: {0}")] + UnsupportedKeyType(String), + #[error("Invalid EC key parameters")] + InvalidEcParameters, + #[error("Failed to decode key")] + DecodeError, + #[error("Failed to encode key")] + EncodeError, + #[error("Failed to validate key pair: {0:?}")] + KeyValidationError(KeyValidationError), +} + +#[derive(Debug, Eq, PartialEq)] +pub struct KeyValidationError(String); + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq)] +#[serde(untagged)] +pub enum Any { + None, + String(Cow<'static, str>), + Bool(bool), + Number(isize), + Array(Vec), + Object(HashMap, Any>), +} + +impl From for Any { + fn from(value: bool) -> Self { + Any::Bool(value) + } +} + +impl From<&'static str> for Any { + fn from(value: &'static str) -> Self { + Any::String(Cow::Borrowed(value)) + } +} + +impl From for Any { + fn from(value: String) -> Self { + Any::String(Cow::Owned(value)) + } +} + +impl From> for Any +where + T: Into, +{ + fn from(value: Option) -> Self { + value.map(T::into).unwrap_or(Any::None) + } +} + +impl From> for Any +where + T: Into, +{ + fn from(value: Vec) -> Self { + Any::Array(value.into_iter().map(T::into).collect()) + } +} + +#[cfg(feature = "python_extension")] +impl<'py> pyo3::FromPyObject<'py> for Any { + fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult { + use pyo3::types::PyAnyMethods; + if ob.is_none() { + return Ok(Any::None); + } + if let Ok(value) = ob.extract::() { + return Ok(Any::Bool(value)); + } + if let Ok(value) = ob.extract::() { + return Ok(Any::Number(value)); + } + if let Ok(value) = ob.extract::() { + return Ok(Any::String(Cow::Owned(value))); + } + let res: Result, pyo3::PyErr> = ob.extract(); + if let Ok(list) = res { + let mut items = Vec::new(); + for item in list { + items.push(Any::extract_bound(&item)?); + } + return Ok(Any::Array(items)); + } + let res: Result, pyo3::PyErr> = ob.extract(); + if let Ok(dict) = res { + let mut items = HashMap::new(); + for (k, v) in dict { + items.insert(Cow::Owned(k.extract::()?), Any::extract_bound(&v)?); + } + return Ok(Any::Object(items)); + } + Err(pyo3::PyErr::new::( + "Invalid Any value", + )) + } +} + +#[cfg(feature = "python_extension")] +impl<'py> pyo3::IntoPyObject<'py> for Any { + type Target = pyo3::PyAny; + type Output = pyo3::Bound<'py, pyo3::PyAny>; + type Error = pyo3::PyErr; + fn into_pyobject(self, py: pyo3::Python<'py>) -> Result { + use pyo3::IntoPyObjectExt; + + Ok(match self { + Any::None => py.None(), + Any::String(s) => s.as_ref().into_py_any(py)?, + Any::Bool(b) => b.into_py_any(py)?, + Any::Number(n) => n.into_py_any(py)?, + Any::Array(a) => a.into_py_any(py)?, + Any::Object(o) => o.into_py_any(py)?, + } + .into_bound(py)) + } +} + +#[cfg(test)] +mod tests { + use std::{ + collections::{HashMap, HashSet}, + time::Duration, + }; + + use super::*; + + #[test] + fn test_key_registry_add_remove() { + let mut registry = KeyRegistry::default(); + registry.add_key(PrivateKey::generate(Some("1".to_owned()), KeyType::HS256).unwrap()); + registry.add_key(PrivateKey::generate(Some("2".to_owned()), KeyType::HS256).unwrap()); + registry.add_key(PrivateKey::generate(Some("3".to_owned()), KeyType::HS256).unwrap()); + assert_eq!(registry.len(), 3); + assert!(!registry.is_empty()); + assert!(registry.remove_kid("1")); + assert_eq!(registry.len(), 2); + assert!(!registry.remove_kid("1")); + assert_eq!(registry.len(), 2); + assert!(registry.remove_kid("2")); + assert_eq!(registry.len(), 1); + assert!(!registry.remove_kid("2")); + assert_eq!(registry.len(), 1); + assert!(registry.remove_kid("3")); + assert_eq!(registry.len(), 0); + assert!(!registry.remove_kid("3")); + } + + #[test] + fn test_key_registry_re_add() { + let mut registry = KeyRegistry::default(); + let key = PrivateKey::generate(Some("1".to_owned()), KeyType::HS256).unwrap(); + + registry.add_key(key.clone_key()); + assert_eq!(registry.len(), 1); + registry.add_key(key.clone_key()); + assert_eq!(registry.len(), 1); + registry.remove_kid("1"); + assert_eq!(registry.len(), 0); + registry.add_key(key); + assert_eq!(registry.len(), 1); + } + + #[test] + fn test_key_registry_add_dupe_kid() { + let mut registry = KeyRegistry::default(); + let key = PrivateKey::generate(Some("1".to_owned()), KeyType::HS256).unwrap(); + registry.add_key(key.clone_key()); + assert_eq!(registry.len(), 1); + registry.add_key(key.clone_key()); + assert_eq!(registry.len(), 1); + + let key2 = PrivateKey::generate(Some("1".to_owned()), KeyType::RS256).unwrap(); + registry.add_key(key2.clone_key()); + assert_eq!(registry.len(), 1); + registry.add_key(key2.clone_key()); + assert_eq!(registry.len(), 1); + } + + #[test] + fn test_sign() { + let key = PrivateKey::generate(Some("1".to_owned()), KeyType::HS256).unwrap(); + let claims = HashMap::from([("hello".to_owned(), "world".into())]); + let signing_ctx = SigningContext { + expiry: Some(Duration::from_secs(10)), + issuer: Some("issuer".to_owned()), + audience: Some("audience".to_owned()), + ..Default::default() + }; + let token = key.sign(claims.clone(), &signing_ctx).unwrap(); + println!("token: {}", token); + let decoded = key.validate(&token, &signing_ctx).unwrap(); + assert_eq!(decoded, claims); + } + + #[test] + fn test_sign_no_expiry() { + let key = PrivateKey::generate(Some("1".to_owned()), KeyType::HS256).unwrap(); + let claims = HashMap::from([("hello".to_owned(), "world".into())]); + let signing_ctx = SigningContext { + issuer: Some("issuer".to_owned()), + audience: Some("audience".to_owned()), + ..Default::default() + }; + let token = key.sign(claims.clone(), &signing_ctx).unwrap(); + let decoded = key.validate(&token, &signing_ctx).unwrap(); + assert_eq!(decoded, claims); + } + + #[test] + fn load_from_empty() { + let mut registry = KeyRegistry::::default(); + let added = registry.add_from_any("").unwrap(); + assert_eq!(added, 0); + registry.add_from_pem("").unwrap(); + assert_eq!(added, 0); + registry.add_from_jwkset("{\"keys\":[]}").unwrap(); + assert_eq!(added, 0); + } + + #[test] + fn test_google_jwkset() { + let mut registry = KeyRegistry::::default(); + let added = registry + .add_from_jwkset(include_str!("testcases/jwkset-goog.json")) + .unwrap(); + assert_eq!(added, 2); + } + + #[test] + fn test_microsoft_jwkset() { + let mut registry = KeyRegistry::::default(); + let added = registry + .add_from_jwkset(include_str!("testcases/jwkset-msft.json")) + .unwrap(); + assert_eq!(added, 9); + } + + #[test] + fn test_slack_jwkset() { + let mut registry = KeyRegistry::::default(); + let added = registry + .add_from_jwkset(include_str!("testcases/jwkset-slck.json")) + .unwrap(); + assert_eq!(added, 1); + } + + #[test] + fn test_apple_jwkset() { + let mut registry = KeyRegistry::::default(); + let added = registry + .add_from_jwkset(include_str!("testcases/jwkset-aapl.json")) + .unwrap(); + assert_eq!(added, 3); + } + + #[test] + fn load_keys_from_jwkset() { + let mut registry = KeyRegistry::::default(); + let added = registry + .add_from_jwkset(include_str!("testcases/jwkset-pub.json")) + .unwrap(); + assert_eq!(added, 0); + let mut registry = KeyRegistry::::default(); + let added = registry + .add_from_jwkset(include_str!("testcases/jwkset-prv.json")) + .unwrap(); + assert_eq!(added, 3); + } + + #[test] + fn load_pub_keys_from_jwkset() { + let mut registry = KeyRegistry::::default(); + let added = registry + .add_from_jwkset(include_str!("testcases/jwkset-pub.json")) + .unwrap(); + assert_eq!(added, 2); + let mut registry = KeyRegistry::::default(); + let added = registry + .add_from_jwkset(include_str!("testcases/jwkset-prv.json")) + .unwrap(); + assert_eq!(added, 3); + } + + #[test] + fn validate_tokens_from_jwkset() { + let mut registry = KeyRegistry::::default(); + registry + .add_from_jwkset(include_str!("testcases/jwkset-prv.json")) + .unwrap(); + let keys = registry.into_keys().collect::>(); + + let mut registry = KeyRegistry::::default(); + registry + .add_from_jwkset(include_str!("testcases/jwkset-prv.json")) + .unwrap(); + + let claims = HashMap::from([("test".to_owned(), "value".into())]); + let signing_ctx = SigningContext { + issuer: Some("test-issuer".to_owned()), + audience: Some("test-audience".to_owned()), + ..Default::default() + }; + + // Generate and validate a token with each key + for key in &keys { + let token = key.sign(claims.clone(), &signing_ctx).unwrap(); + let decoded = registry.validate(&token, &signing_ctx).unwrap(); + assert_eq!(decoded, claims); + } + + // Generate and validate a token with each key against the public keys + let mut registry = KeyRegistry::::default(); + registry + .add_from_jwkset(include_str!("testcases/jwkset-prv.json")) + .unwrap(); + for key in &keys { + let token = key.sign(claims.clone(), &signing_ctx).unwrap(); + let decoded = registry.validate(&token, &signing_ctx).unwrap(); + assert_eq!(decoded, claims); + } + } + + #[test] + fn test_validate_tokens_from_jwkset_named() { + let mut key1 = PrivateKey::generate(Some("1".to_owned()), KeyType::HS256).unwrap(); + let mut key2 = PrivateKey::generate(Some("2".to_owned()), KeyType::HS256).unwrap(); + + let claims = HashMap::from([("test".to_owned(), "value".into())]); + let signing_ctx = SigningContext { + issuer: Some("test-issuer".to_owned()), + audience: Some("test-audience".to_owned()), + ..Default::default() + }; + let token = key1.sign(claims, &signing_ctx).unwrap(); + + // Swap the keys so the signature is no longer valid with the specified kid + key1.set_kid(Some("2".to_owned())); + key2.set_kid(Some("1".to_owned())); + + let mut registry = KeyRegistry::::default(); + registry.add_key(key1); + registry.add_key(key2); + + let decoded = registry.validate(&token, &signing_ctx).unwrap_err(); + assert_eq!( + decoded, + ValidationError::Invalid(OpaqueValidationFailureReason::InvalidSignature), + "{}", + decoded.error_string_not_for_user() + ); + } + + #[test] + fn test_validate_tokens_from_jwkset_named_allow_deny() { + let key = PrivateKey::generate(Some("1".to_owned()), KeyType::HS256).unwrap(); + let mut registry = KeyRegistry::::default(); + registry.add_key(key); + + let claims = HashMap::from([("jti".to_owned(), "1234".into())]); + let signing_ctx = SigningContext { + allow: HashMap::from([("jti".to_owned(), HashSet::from(["1234".to_owned()]))]), + ..Default::default() + }; + + let token = registry.sign(claims.clone(), &signing_ctx).unwrap(); + let decoded = registry.validate(&token, &signing_ctx).unwrap(); + assert_eq!(decoded, claims); + + let claims = HashMap::from([("jti".to_owned(), "bad".into())]); + let token = registry.sign(claims, &signing_ctx).unwrap(); + let decoded = registry.validate(&token, &signing_ctx).unwrap_err(); + + assert_eq!( + decoded, + ValidationError::Invalid(OpaqueValidationFailureReason::InvalidClaimValue( + "jti".to_string(), + Some("bad".to_string()) + )) + ); + } + + #[test] + fn test_any_json() { + let map: HashMap = HashMap::from([ + ("hello".to_owned(), "world".into()), + ("empty".to_owned(), Any::None), + ("bool".to_owned(), Any::Bool(true)), + ("number".to_owned(), Any::Number(123)), + ( + "array".to_owned(), + Any::Array(vec![Any::String("1".into()), Any::String("2".into())].into()), + ), + ]); + let json = serde_json::to_string(&map).unwrap(); + assert!(json.contains("\"hello\":\"world\"")); + assert!(json.contains("\"empty\":null")); + assert!(json.contains("\"bool\":true")); + assert!(json.contains("\"number\":123")); + assert!(json.contains("\"array\":[\"1\",\"2\"]")); + } +} diff --git a/rust/gel-jwt/src/python.rs b/rust/gel-jwt/src/python.rs new file mode 100644 index 000000000000..341bd2a5903a --- /dev/null +++ b/rust/gel-jwt/src/python.rs @@ -0,0 +1,378 @@ +use std::{ + collections::HashMap, + time::{Duration, Instant}, +}; + +use crate::{ + bare_key::SerializedKey, Any, BarePrivateKey, Key, KeyError, KeyRegistry, KeyType, + SignatureError, ValidationError, +}; +use pyo3::{ + exceptions::PyValueError, + prelude::*, + types::{PyBytes, PyDict, PyList}, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +impl From for PyErr { + fn from(value: KeyError) -> Self { + PyValueError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: SignatureError) -> Self { + PyValueError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: ValidationError) -> Self { + PyValueError::new_err(value.to_string() + ":" + value.error_string_not_for_user().as_str()) + } +} + +#[pyclass] +pub struct SigningCtx { + context: crate::SigningContext, +} + +#[pymethods] +impl SigningCtx { + #[new] + pub fn new() -> PyResult { + Ok(Self { + context: crate::SigningContext::default(), + }) + } + + pub fn set_issuer(&mut self, issuer: &str) { + self.context.issuer = Some(issuer.to_string()); + } + + pub fn set_audience(&mut self, audience: &str) { + self.context.audience = Some(audience.to_string()); + } + + pub fn set_not_before(&mut self, not_before: usize) { + self.context.not_before = Some(Duration::from_secs(not_before as u64)); + } + + pub fn set_expiry(&mut self, expiry: usize) { + self.context.expiry = Some(Duration::from_secs(expiry as u64)); + } + + pub fn allow(&mut self, claim: &str, values: Bound) -> PyResult<()> { + self.context + .allow + .insert(claim.to_string(), values.extract()?); + Ok(()) + } + + pub fn deny(&mut self, claim: &str, values: Bound) -> PyResult<()> { + self.context + .deny + .insert(claim.to_string(), values.extract()?); + Ok(()) + } +} + +#[pyclass] +pub struct JWKSet { + registry: KeyRegistry, + context: crate::SigningContext, +} + +#[pymethods] +impl JWKSet { + #[new] + pub fn new() -> PyResult { + let registry = KeyRegistry::::default(); + Ok(Self { + registry, + context: crate::SigningContext::default(), + }) + } + + #[staticmethod] + pub fn from_hs256_key(key: Bound) -> PyResult { + let key = BarePrivateKey::from_raw_oct(key.as_bytes())?; + let mut registry = KeyRegistry::::default(); + registry.add_key(Key::from_bare_private_key(None, key)?); + Ok(Self { + registry, + context: crate::SigningContext::default(), + }) + } + + #[pyo3(signature = (*, kid, kty))] + pub fn generate(&mut self, kid: Option<&str>, kty: &str) -> PyResult<()> { + let key = match kty { + "HS256" => BarePrivateKey::generate(KeyType::HS256), + "RS256" => BarePrivateKey::generate(KeyType::RS256), + "ES256" => BarePrivateKey::generate(KeyType::ES256), + _ => return Err(PyValueError::new_err("Invalid key type")), + }?; + self.registry + .add_key(Key::from_bare_private_key(kid.map(String::from), key)?); + Ok(()) + } + + #[pyo3(signature = (*, kid, kty, **kwargs))] + pub fn add( + &mut self, + kid: Option<&str>, + kty: &str, + kwargs: Option>, + ) -> PyResult<()> { + let mut map = serde_json::Map::default(); + if let Some(kwargs) = kwargs { + for (key, value) in kwargs.iter() { + let key = key.extract::()?; + let value = value.extract::()?; + map.insert(key, value.into()); + } + } + if let Some(kid) = kid { + map.insert("kid".to_string(), kid.to_string().into()); + } + let kty = match kty { + "HS256" => "oct", + "RS256" => "RSA", + "ES256" => "EC", + _ => return Err(PyValueError::new_err("Invalid key type")), + }; + map.insert("kty".to_string(), kty.to_string().into()); + let key: SerializedKey = serde_json::from_value(serde_json::Value::Object(map)) + .map_err(|e| PyValueError::new_err(format!("Error creating key: {e}")))?; + match key { + SerializedKey::Private(kid, key) => { + self.registry.add_key(Key::from_bare_private_key(kid, key)?); + } + SerializedKey::Public(kid, key) => { + self.registry.add_key(Key::from_bare_public_key(kid, key)?); + } + SerializedKey::UnknownOrInvalid(error, _, _) => { + return Err(PyValueError::new_err(format!("Invalid key: {error}"))); + } + } + Ok(()) + } + + pub fn load(&mut self, keys: &str) -> PyResult { + let count = self.registry.add_from_any(keys)?; + Ok(count) + } + + pub fn load_json(&mut self, keys: &str) -> PyResult { + let count = self.registry.add_from_jwkset(keys)?; + Ok(count) + } + + pub fn set_issuer(&mut self, issuer: &str) { + self.context.issuer = Some(issuer.to_string()); + } + + pub fn set_audience(&mut self, audience: &str) { + self.context.audience = Some(audience.to_string()); + } + + pub fn set_not_before(&mut self, not_before: usize) { + self.context.not_before = Some(Duration::from_secs(not_before as u64)); + } + + pub fn set_expiry(&mut self, expiry: usize) { + self.context.expiry = Some(Duration::from_secs(expiry as u64)); + } + + pub fn allow(&mut self, claim: &str, values: Bound) -> PyResult<()> { + self.context + .allow + .insert(claim.to_string(), values.extract()?); + Ok(()) + } + + pub fn deny(&mut self, claim: &str, values: Bound) -> PyResult<()> { + self.context + .deny + .insert(claim.to_string(), values.extract()?); + Ok(()) + } + + #[pyo3(signature = (*, private_keys=true))] + pub fn export_pem(&self, private_keys: bool) -> PyResult> { + if private_keys { + Ok(self.registry.to_pem().into_bytes()) + } else { + Ok(self.registry.to_pem_public()?.into_bytes()) + } + } + + #[pyo3(signature = (*, private_keys=true))] + pub fn export_json(&self, private_keys: bool) -> PyResult> { + Ok(if private_keys { + self.registry.to_json()? + } else { + self.registry.to_json_public()? + } + .into_bytes()) + } + + pub fn can_sign(&self) -> bool { + self.registry.can_sign() + } + + /// Sign a claims object with the default or given signing context. + #[pyo3(signature = (claims, *, ctx=None))] + pub fn sign(&self, claims: Bound, ctx: Option<&SigningCtx>) -> PyResult { + let claims = claims.extract()?; + let token = self + .registry + .sign(claims, ctx.map(|c| &c.context).unwrap_or(&self.context))?; + Ok(token) + } + + pub fn validate(&self, token: &str) -> PyResult> { + let claims = self.registry.validate(token, &self.context)?; + Ok(claims) + } + + pub fn __repr__(&self) -> String { + format!("JWKSet(keys={})", self.registry.len()) + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +struct GelClaims { + #[serde(rename = "edb.i")] + instances: Option>, + #[serde(rename = "edb.i.all")] + all_instances: bool, + #[serde(rename = "edb.r")] + roles: Option>, + #[serde(rename = "edb.r.all")] + all_roles: bool, + #[serde(rename = "edb.d")] + databases: Option>, + #[serde(rename = "edb.d.all")] + all_databases: bool, + #[serde(rename = "jti")] + jti: uuid::Uuid, +} + +fn vec_from_list_or_tuple(value: Bound) -> PyResult> { + if let Ok(list) = value.extract::>() { + Ok(list) + } else { + let mut list = Vec::new(); + let iter = value.try_iter()?; + for item in iter { + let item = item?; + if let Ok(item) = item.extract::() { + list.push(item); + } else { + return Err(PyValueError::new_err( + "Expected a list or other iterable of strings", + )); + } + } + Ok(list) + } +} + +/// A very basic cache for JWKSets. +#[pyclass] +pub struct JWKSetCache { + cache: HashMap)>, + expiry_seconds: usize, +} + +#[pymethods] +impl JWKSetCache { + #[new] + pub fn new(expiry_seconds: usize) -> PyResult { + Ok(Self { + cache: HashMap::new(), + expiry_seconds, + }) + } + + /// Get a JWKSet from the cache and returns whether the cache is fresh or stale. + pub fn get(&mut self, py: Python, key: &str) -> PyResult<(bool, Option>)> { + if let Some((expiry, registry)) = self.cache.get_mut(key) { + if Instant::now() > *expiry { + // Temporarily extend the expiry time by 60 seconds to avoid multiple fetches + *expiry = Instant::now() + Duration::from_secs(60); + return Ok((false, Some(registry.clone_ref(py)))); + } else { + return Ok((true, Some(registry.clone_ref(py)))); + } + } + Ok((false, None)) + } + + /// Set a JWKSet in the cache, resetting the expiry time. + pub fn set(&mut self, key: &str, registry: Py) { + self.cache.insert( + key.to_string(), + ( + Instant::now() + Duration::from_secs(self.expiry_seconds as _), + registry, + ), + ); + } +} + +#[pyfunction] +#[pyo3(signature = (registry, *, instances=None, roles=None, databases=None))] +fn generate_gel_token( + registry: &JWKSet, + instances: Option>, + roles: Option>, + databases: Option>, +) -> PyResult { + let mut claims = GelClaims::default(); + + if let Some(instances) = instances { + claims.instances = Some(vec_from_list_or_tuple(instances)?); + } else { + claims.all_instances = true; + } + + if let Some(roles) = roles { + claims.roles = Some(vec_from_list_or_tuple(roles)?); + } else { + claims.all_roles = true; + } + + if let Some(databases) = databases { + claims.databases = Some(vec_from_list_or_tuple(databases)?); + } else { + claims.all_databases = true; + } + + claims.jti = Uuid::new_v4(); + + let claims = HashMap::from([ + ("edb.i".to_string(), Any::from(claims.instances)), + ("edb.i.all".to_string(), Any::from(claims.all_instances)), + ("edb.r".to_string(), Any::from(claims.roles)), + ("edb.r.all".to_string(), Any::from(claims.all_roles)), + ("edb.d".to_string(), Any::from(claims.databases)), + ("edb.d.all".to_string(), Any::from(claims.all_databases)), + ("jti".to_string(), Any::from(claims.jti.to_string())), + ]); + + let token = registry.registry.sign(claims, ®istry.context)?; + Ok(format!("edbt1_{}", token)) +} + +#[pymodule] +pub fn _jwt(_py: Python, m: &Bound) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(generate_gel_token, m)?)?; + Ok(()) +} diff --git a/rust/gel-jwt/src/registry.rs b/rust/gel-jwt/src/registry.rs new file mode 100644 index 000000000000..9daddf7d2dbf --- /dev/null +++ b/rust/gel-jwt/src/registry.rs @@ -0,0 +1,318 @@ +use crate::{ + bare_key::{SerializedKey, SerializedKeys}, + key::*, + Any, KeyError, OpaqueValidationFailureReason, SignatureError, ValidationError, +}; +use std::{ + collections::{BTreeSet, HashMap, HashSet}, + fmt::Debug, +}; + +pub(crate) trait IsKey { + type Inner: std::hash::Hash + Eq + Debug + Clone; + + fn inner(&self) -> &Self::Inner; + fn key_type(inner: &Self::Inner) -> KeyType; + fn from_inner(kid: Option, inner: Self::Inner) -> Self; + fn into_inner(self) -> (Option, Self::Inner); + fn get_serialized_key(key: SerializedKey) -> Option + where + Self: Sized; + fn to_serialized_key(kid: Option<&str>, inner: &Self::Inner) -> SerializedKey; + fn from_pem(pem: &str) -> Result>, KeyError> + where + Self: Sized; + fn to_pem(inner: &Self::Inner) -> String; + fn encoding_key(inner: &Self::Inner) -> Option<&jsonwebtoken::EncodingKey>; + fn decoding_key(inner: &Self::Inner) -> &jsonwebtoken::DecodingKey; +} + +/// A collection of [`Key`] or [`PublicKey`] objects. +#[allow(private_bounds)] +pub struct KeyRegistry { + // TODO: this could probably be optimized, especially if we can + // generate key signatures. + /// Map from key identifier (kid) to ordinal + named_keys: HashMap, + /// Set of ordinals for unnamed keys + unnamed_keys: HashSet, + /// Map from key to ordinal/kid for quick lookup + key_to_ordinal: HashMap)>, + /// Set of active ordinals + active_keys: BTreeSet, + /// Next ordinal to use for a new key + next: usize, +} + +impl Default for KeyRegistry { + fn default() -> Self { + Self { + named_keys: HashMap::default(), + unnamed_keys: HashSet::default(), + key_to_ordinal: HashMap::default(), + active_keys: BTreeSet::default(), + next: 0, + } + } +} + +#[allow(private_bounds)] +impl KeyRegistry { + /// Clear the registry. + pub fn clear(&mut self) { + *self = Self::default(); + } + + pub fn into_keys(self) -> impl Iterator { + self.key_to_ordinal + .into_iter() + .map(|(key, (_, kid))| K::from_inner(kid, key)) + } + + /// Add a key to the registry. If the key already exists, it will be + /// replaced. If the key specifies the same kid as another key already + /// added, the new key will replace the old one. + /// + /// Adding a key, even if it already exists, will make it the active key. + pub fn add_key(&mut self, key: K) { + self.remove_key(&key); + + let (kid, inner) = key.into_inner(); + + // If the kid still exists, we need to remove that key too + if let Some(kid) = &kid { + if self.named_keys.contains_key(kid) { + self.remove_kid(kid); + } + } + + // Key is new, add it to the registry + let ordinal = self.next; + self.next += 1; + self.key_to_ordinal.insert(inner, (ordinal, kid.clone())); + self.active_keys.insert(ordinal); + + if let Some(kid) = kid { + self.named_keys.insert(kid, ordinal); + } else { + self.unnamed_keys.insert(ordinal); + } + } + + /// Remove a key from the registry by its key. + pub fn remove_key(&mut self, key: &K) { + let inner = key.inner(); + if let Some((ordinal, kid)) = self.key_to_ordinal.remove(inner) { + if let Some(kid) = kid { + self.named_keys.remove(&kid); + } else { + self.unnamed_keys.remove(&ordinal); + } + self.active_keys.remove(&ordinal); + } + } + + /// Remove a key from the registry by its kid. Note: O(N). + pub fn remove_kid(&mut self, kid: &str) -> bool { + if let Some(ordinal) = self.named_keys.remove(kid) { + self.active_keys.remove(&ordinal); + self.key_to_ordinal.retain(|_, &mut (v, _)| v != ordinal); + true + } else { + false + } + } + + /// Get the number of keys in the registry. + pub fn len(&self) -> usize { + self.key_to_ordinal.len() + } + + /// Check if the registry is empty. + pub fn is_empty(&self) -> bool { + self.key_to_ordinal.is_empty() + } + + /// Add keys from a JWKSet. + pub fn add_from_jwkset(&mut self, jwkset: &str) -> Result { + let loaded: SerializedKeys = + serde_json::from_str(jwkset).map_err(|_| KeyError::InvalidJson)?; + let mut added = 0; + for key in loaded.keys { + if let Some(key) = K::get_serialized_key(key) { + self.add_key(key); + added += 1; + } else { + // TODO: log unknown or invalid key + } + } + Ok(added) + } + + /// Add keys from a PEM file. + pub fn add_from_pem(&mut self, pem: &str) -> Result { + let keys = K::from_pem(pem)?; + let mut added = 0; + for key in keys { + if let Ok(key) = key { + self.add_key(key); + added += 1; + } else { + // TODO: log unknown or invalid key + } + } + Ok(added) + } + + /// Add keys from a source string which can be either a JWK set or a PEM file with + /// 1 or more keys. + pub fn add_from_any(&mut self, source: &str) -> Result { + let source = source.trim(); + if source.is_empty() { + return Ok(0); + } + + // Get the first non-whitespace character + let first_char = source.chars().next().unwrap_or_default(); + if first_char == '{' { + self.add_from_jwkset(source) + } else if first_char == '-' { + self.add_from_pem(source) + } else { + Err(KeyError::UnsupportedKeyType(format!( + "Expected JWK set or PEM file, got {}", + first_char + ))) + } + } + + pub fn to_pem(&self) -> String { + let mut pem = String::new(); + for (k, (_, _)) in &self.key_to_ordinal { + pem.push_str(&K::to_pem(k)); + } + pem + } + + pub fn to_json(&self) -> Result { + serde_json::to_string(&SerializedKeys { + keys: self + .key_to_ordinal + .iter() + .map(|(k, (_, kid))| K::to_serialized_key(kid.as_deref(), k)) + .collect(), + }) + .map_err(|_| KeyError::EncodeError) + } + + /// Get the active key and kid. + fn active_key(&self) -> Option<(Option<&str>, &K::Inner)> { + if let Some(&i) = self.active_keys.last() { + for (k, &(v, ref kid)) in &self.key_to_ordinal { + if v == i { + if let Some(kid) = kid { + return Some((Some(kid.as_str()), k)); + } else { + return Some((None, k)); + } + } + } + } + None + } + + pub fn validate( + &self, + token: &str, + ctx: &SigningContext, + ) -> Result, ValidationError> { + // If we have a named key that matches, use that. + if !self.named_keys.is_empty() { + if let Ok(header) = jsonwebtoken::decode_header(token) { + if let Some(header_kid) = header.kid { + for (key, (_, kid)) in &self.key_to_ordinal { + if kid.as_deref() == Some(header_kid.as_str()) { + return validate_token( + K::key_type(key), + K::decoding_key(key), + None, + token, + ctx, + ); + } + } + } + } + } + + let mut result = None; + for (key, _) in self.key_to_ordinal.iter() { + let last_result = + validate_token(K::key_type(key), K::decoding_key(key), None, token, ctx); + match last_result { + Ok(result) => return Ok(result), + Err(e) => result = Some(e), + } + } + Err(result.unwrap_or(OpaqueValidationFailureReason::NoAppropriateKey.into())) + } + + pub fn can_sign(&self) -> bool { + self.active_key() + .map(|(_, k)| K::encoding_key(k).is_some()) + .unwrap_or(false) + } + + pub fn sign( + &self, + claims: HashMap, + ctx: &SigningContext, + ) -> Result { + let (kid, key) = self.active_key().ok_or(SignatureError::NoAppropriateKey)?; + let encoding_key = K::encoding_key(key).ok_or(SignatureError::NoAppropriateKey)?; + sign_token(K::key_type(key), encoding_key, kid, claims, ctx) + } +} + +impl KeyRegistry {} + +impl KeyRegistry {} + +impl KeyRegistry { + /// Export the registry as a PEM file containing only the public keys. + /// This will fail if the registry contains symmetric keys. + pub fn to_pem_public(&self) -> Result { + let mut pem = String::new(); + for (k, (_, _)) in &self.key_to_ordinal { + match k { + KeyInner::Private(k) => { + pem.push_str(&k.bare_key.to_pem_public()?); + } + KeyInner::Public(k) => { + pem.push_str(&k.bare_key.to_pem()); + } + } + } + Ok(pem) + } + + /// Export the registry as a JSON object containing only the public keys. + /// This will fail if the registry contains symmetric keys. + pub fn to_json_public(&self) -> Result { + let mut keys = Vec::new(); + for (k, (_, kid)) in &self.key_to_ordinal { + match k { + KeyInner::Private(k) => { + keys.push(SerializedKey::Public( + kid.clone(), + k.bare_key.to_public()?.clone_key(), + )); + } + KeyInner::Public(k) => { + keys.push(SerializedKey::Public(kid.clone(), k.bare_key.clone_key())); + } + } + } + serde_json::to_string(&SerializedKeys { keys }).map_err(|_| KeyError::EncodeError) + } +} diff --git a/rust/gel-jwt/src/testcases/gen.sh b/rust/gel-jwt/src/testcases/gen.sh new file mode 100755 index 000000000000..c0a7fc925540 --- /dev/null +++ b/rust/gel-jwt/src/testcases/gen.sh @@ -0,0 +1,56 @@ +#!/bin/bash +OLD_PWD=$(pwd) +trap "cd $OLD_PWD" EXIT + +cd $(dirname $0) + +rm *.pem +rm *.asn1.txt +# Generate RSA 2048 private keys in PKCS8 and PKCS1 formats +openssl genpkey -algorithm RSA -out rsa2048-prv-pkcs8.pem -pkeyopt rsa_keygen_bits:2048 +openssl rsa -in rsa2048-prv-pkcs8.pem -out rsa2048-prv-pkcs1.pem -outform PEM -traditional + +# Generate RSA 2048 public keys in PKCS8 and PKCS1 formats +openssl rsa -in rsa2048-prv-pkcs8.pem -out rsa2048-pub-pkcs8.pem -outform PEM -pubout +openssl rsa -in rsa2048-prv-pkcs1.pem -out rsa2048-pub-pkcs1.pem -outform PEM -pubout -RSAPublicKey_out + +# Generate prime256v1 private keys in SEC1 and PKCS8 formats +openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-prv-sec1.pem +openssl pkcs8 -topk8 -inform PEM -outform PEM -in prime256v1-prv-sec1.pem -out prime256v1-prv-pkcs8.pem -nocrypt + +# Generate prime256v1 public keys in various formats +# SPKI format (compressed) +openssl ec -in prime256v1-prv-sec1.pem -pubout -out prime256v1-pub-spki.pem -conv_form compressed + +# SPKI format (uncompressed) +openssl ec -in prime256v1-prv-sec1.pem -pubout -out prime256v1-pub-spki-uncompressed.pem -conv_form uncompressed + +# Raw EC point formats (display only) +echo "Compressed public key point:" +openssl ec -in prime256v1-prv-sec1.pem -text -noout -conv_form compressed | grep 'pub:' -A 2 + +echo "Uncompressed public key point:" +openssl ec -in prime256v1-prv-sec1.pem -text -noout -conv_form uncompressed | grep 'pub:' -A 2 + +# For each file, run asn1parse and save the output to a file with the same name but -asn1.txt extension +for file in *.pem; do + # First do basic asn1parse + openssl asn1parse -dump -in $file > ${file%.pem}-asn1.txt + + # Look for any BITSTRING or OCTET STRING fields and parse them + while read -r line; do + if [[ $line =~ "BIT STRING" ]] || [[ $line =~ "OCTET STRING" ]]; then + # Extract offset and length + offset=$(echo $line | cut -d':' -f1 | tr -d ' ') + # Parse the contents + if [[ $line =~ "BIT STRING" ]]; then + echo "-- BITSTRING at offset $offset --" >> ${file%.pem}-asn1-tmp.txt + else + echo "-- OCTET STRING at offset $offset --" >> ${file%.pem}-asn1-tmp.txt + fi + openssl asn1parse -dump -in $file -strparse $offset >> ${file%.pem}-asn1-tmp.txt 2>/dev/null + fi + done < ${file%.pem}-asn1.txt + cat ${file%.pem}-asn1-tmp.txt >> ${file%.pem}-asn1.txt 2>/dev/null + rm ${file%.pem}-asn1-tmp.txt 2>/dev/null +done diff --git a/rust/gel-jwt/src/testcases/jwkset-aapl.json b/rust/gel-jwt/src/testcases/jwkset-aapl.json new file mode 100644 index 000000000000..a153c66e6a1d --- /dev/null +++ b/rust/gel-jwt/src/testcases/jwkset-aapl.json @@ -0,0 +1,28 @@ +{ + "keys": [ + { + "kty": "RSA", + "kid": "dMlERBaFdK", + "use": "sig", + "alg": "RS256", + "n": "ryLWkB74N6SJNRVBjKF6xKMfP-QW3AAsJotv0LjVtf7m4NZNg_gTL78e7O8wmvngF8FuzBrvqf1mGW17Ct8BgNK6YXxnoGL0YLmlwXbmCZvTXki0VlEW1PDXeViWy7qXaCp2caF5v4OOdPsgroxNO_DgJRTuA_izJ4DFZYHCHXwojfdWJiDYG67j5PlD5pXKGx7zaqyryjovZTEII_Z1_bhFCRUZRjfJ3TVoK0fZj2z7iAZWjn33i-V3zExUhwzEyeuGph0118NfmOLCUEy_Jd4xvLf_X4laPpe9nq8UeORfs72yz2qH7cHDKL85W6oG08Gu05JWuAs5Ay49WxJrmw", + "e": "AQAB" + }, + { + "kty": "RSA", + "kid": "rs0M3kOV9p", + "use": "sig", + "alg": "RS256", + "n": "zH5so3zLsgmRypxAAYJimfF9cx3ISSyHyzjDP3yvE9ieqpnjFJhzgCP8L4oKO9vUFNpoG1ub7I3paYNY6Vb2yc4chnsjJxB3j0jomJ3iI9MlWoVecTFG2tywyx5NRhy3YfTUpw2uCLafzWrpIJIoKUCGM6iUgaIFjvfi-cGT5T_5eUSWZHN-ziH69mGcbMRGLQEixQUatwru9i4i-OSk-w-JmLOqAzRP1mVn1tcZRIoGSB2PFSSJX9SK90OX8i5sj7dpIO_2xbGMtyNJkDzGq88x1pMJ4sv6HMj-tx4QrpGDbUi7zBCgbBnNSGSB_LBv4dbswwWY96ckHgx9yf_7IQ", + "e": "AQAB" + }, + { + "kty": "RSA", + "kid": "rBRfVmqsjn", + "use": "sig", + "alg": "RS256", + "n": "pPOaiF5yL-y42FaKg9PYASR5-rdTK7NEiteNUAzNp0zkta-HW-tgLNLNlsft3zcrsgOLqXxhX7qzlI3JGH-wSs7_v2XNSg57QhOTxPDqtUfy5DegtiSOgwE947OBTwCWo2R6cGZD1T8ysfO2HuKheq2hEwZU4Y-8qT19WWOhZHs4CVt7A5mzpIgWuUVw766VTyqrqKev32DOUPIqFocFz3tuty95S9t_OYnaPCcET-b6DV_eT7psPhqhl5nNUm0lzkCQ53-9kxQNJxBciy0wiBcAexD4KppKRRD3evFpOSxD1R6Kg2DIG5UnbVVqn5nhZA9RH-t50f_biqV3KlSHJQ", + "e": "AQAB" + } + ] +} \ No newline at end of file diff --git a/rust/gel-jwt/src/testcases/jwkset-goog.json b/rust/gel-jwt/src/testcases/jwkset-goog.json new file mode 100644 index 000000000000..dee94af371a8 --- /dev/null +++ b/rust/gel-jwt/src/testcases/jwkset-goog.json @@ -0,0 +1,20 @@ +{ + "keys": [ + { + "n": "pleuF0RyDsETygZn89RpGVFNMxG_hdYVnvbHadvM1tYxs9ghDq93NFxejt--1QlwpLQ3yuVY_CKldkAWgzPVl8-oUBe5xh9jzpLUTqcyrS1aFLuzAe13-OTadUE18wvhz9goQf80rg5IztD_gBePOOBE7eWHGqWLghuMb7cIYjgFxqNFyPn8bF_7k8pQAeHIPua_6_GHhw3ML4msp-aU7O1io3Z4P_Bir_6_C5J9UtWAcJ0Ez0YC5FxOMkh27joO5mUas8krGnFqIJTOgDYXQC1QTu-HOCRNvi6gFMqEkDTP5oBK2cDPDq5L0T8Q0UanSPR0BuOTHesCXnDAdxdyXw", + "use": "sig", + "kty": "RSA", + "kid": "fa072f75784642615087c7182c101341e18f7a3a", + "alg": "RS256", + "e": "AQAB" + }, + { + "e": "AQAB", + "n": "5D9Xb4z8eFr-3Zh3m5GnM_KVqc6rskPL7EMa6lSxNiMJ-PhXGORU-S-QgLmMvHu3vAMfvxz6ph3JZDpdGT68wj-vWqqBudaDYCbnbkjXm6UpcrFMpGAiOS6gACNxpz80JXaO2DPtl9jTN6WyJY9tLHdqRfesfOlwzB0lmVZ8shSDh8usN3vB1KfYuR6Vytly1phaWJr92yMICKUjtXT-0SlrtqDgX_U2Swl4QyZN6rrfuG3F6Fmw-m12Ve_kyoPUb02bbJCSFDnIZsMvRlSZem5nUrs86zDPTWfNcB0LUYG8OgMzOev7r04h_RY2F6K7c8nE2EobYTrH0kw2QIf8vQ", + "use": "sig", + "kty": "RSA", + "kid": "eec534fa5b8caca201ca8d0ff96b54c562210d1e", + "alg": "RS256" + } + ] +} diff --git a/rust/gel-jwt/src/testcases/jwkset-msft.json b/rust/gel-jwt/src/testcases/jwkset-msft.json new file mode 100644 index 000000000000..1f01cfca8537 --- /dev/null +++ b/rust/gel-jwt/src/testcases/jwkset-msft.json @@ -0,0 +1 @@ +{"keys":[{"kty":"RSA","use":"sig","kid":"YTceO5IJyyqR6jzDS5iAbpe42Jw","x5t":"YTceO5IJyyqR6jzDS5iAbpe42Jw","n":"ieQwBxFWmWZaQ32uVFUu8DiwrWMG9U95AKJMTruqVSKf8bIy25MKo25YntqW8etdn5IBCehDfSrPNtjGT0uIsiRdGVfqJFvTfqszWxho0nNN8RUecVuqSRSmCaaV9sCBcU40d1rXtNoHbh5V2hDSfKAbLAldN0DgW3Anp_YsMfGr6qrSuE4Ynm2P2k0e_2eKfhcvS9fe2_WNb_6sr09ZedY4yQVUVaiW16OIxlwcVesXxc3iQV19T9uAtlajtpCuiAQsu47lEjuggSDmIgl8B0i67UCUWCNezz4ylcQQRjUi-ba95HMYfnFPDkapZzK16Qx2IHoVzzytOfse7ZqFUQ","e":"AQAB","x5c":["MIIC/jCCAeagAwIBAgIJAJZMx/LQIwFdMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjUwMTA0MDQzNDM0WhcNMzAwMTA0MDQzNDM0WjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAieQwBxFWmWZaQ32uVFUu8DiwrWMG9U95AKJMTruqVSKf8bIy25MKo25YntqW8etdn5IBCehDfSrPNtjGT0uIsiRdGVfqJFvTfqszWxho0nNN8RUecVuqSRSmCaaV9sCBcU40d1rXtNoHbh5V2hDSfKAbLAldN0DgW3Anp/YsMfGr6qrSuE4Ynm2P2k0e/2eKfhcvS9fe2/WNb/6sr09ZedY4yQVUVaiW16OIxlwcVesXxc3iQV19T9uAtlajtpCuiAQsu47lEjuggSDmIgl8B0i67UCUWCNezz4ylcQQRjUi+ba95HMYfnFPDkapZzK16Qx2IHoVzzytOfse7ZqFUQIDAQABoyEwHzAdBgNVHQ4EFgQUgYhwGR0+XNfmFTvUNMGZmR9iQP4wDQYJKoZIhvcNAQELBQADggEBAGd81nj7DhZrh9lPdEoMCMbc2BwWphfq28n18CH2wUKmzadGLbQgcjD0QLrNpJUeI18511PCjE4AhzD7cXY+1m0AMYEn63wDDa4uDrgFpVMUZtnvdgWgPytLgqLmAN1O7n5TirOBPh5mbOab2hZXUe9PlRT/F8UJRWqtH5yA0LloZ0wJ30wChwfC2Jc8Q79S7kS8ZnHqMFS/WW8FYbAl6v/mK1UqBUp2QNtZIwSMsAHa9cfUq5o5CxI3oaj85pCvfz4SF4XGupuq5zTYKvbOQJ8ggikItbZfwum23UKFHv9J3sxWgimeH0A5TUPw1iWbrQ7dCkEr5omzt15yfUf26rs="],"cloud_instance_name":"microsoftonline.com","issuer":"https://login.microsoftonline.com/{tenantid}/v2.0"},{"kty":"RSA","use":"sig","kid":"dH-ot5V2o9ccrQBIB4LAkkPmMUg","x5t":"dH-ot5V2o9ccrQBIB4LAkkPmMUg","n":"0YsOoA0v5HD_XzOwLHfJcGWN6-vVdAoJtaTPl9QkKk9M2KQVAxzPS5TKFsbBXftg4KmoaOAPKRtz8xphsqXLsUeauDSaP5jEgBO24pvlQG4Rlea6ZtxDsNK8va0RMAU8IsL1CuqJN73BwBjYwZl9j8QB06decCxeRVF-BeFKfi0cVM_ZO_v17TXGZjXziGxJlx6xhH96s9p0sYD5-tCOQRJaoRZH2JBm3mhYEFomIRTKmjvrzQLgzShO71PL4SnFj79Ye6LoWzfjhG3urnpspFZ3ds2oO1oHCGaJ4d5RP2sDx04ucfntDgZGmO5qqNUZhWxPQZ4aWlvbbMsroSJuvQ","e":"AQAB","x5c":["MIIC/jCCAeagAwIBAgIJAKkxMJro15JaMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjUwMTA4MDY0ODExWhcNMzAwMTA4MDY0ODExWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YsOoA0v5HD/XzOwLHfJcGWN6+vVdAoJtaTPl9QkKk9M2KQVAxzPS5TKFsbBXftg4KmoaOAPKRtz8xphsqXLsUeauDSaP5jEgBO24pvlQG4Rlea6ZtxDsNK8va0RMAU8IsL1CuqJN73BwBjYwZl9j8QB06decCxeRVF+BeFKfi0cVM/ZO/v17TXGZjXziGxJlx6xhH96s9p0sYD5+tCOQRJaoRZH2JBm3mhYEFomIRTKmjvrzQLgzShO71PL4SnFj79Ye6LoWzfjhG3urnpspFZ3ds2oO1oHCGaJ4d5RP2sDx04ucfntDgZGmO5qqNUZhWxPQZ4aWlvbbMsroSJuvQIDAQABoyEwHzAdBgNVHQ4EFgQUQeFcxPOrX3Y9CRQjD4DPEKk14XQwDQYJKoZIhvcNAQELBQADggEBADRmgL+djEZm/K/zqMpC/yhKPhPA3uXWz2ttwQElutWAMLczom6czKFTBfh3XMMj5gpjJesNC72DRSbMXmQ6QbV9i1y7i0yMvnWeHBJe+gU5ocpN3EiGg+I19Gd/wARDq7R7QQUT/+YM3h0i6KnaE5YPp6q1cqX4PXg+rSAc1uKVQ80+S2ibMObYdflxCLb8SElmgWKzi4yZvsaA0hfUi97vwtsotoOdpAuXWOLk28TJqkxE0raZqGUZeT+5WP2uR7VTArbZxEwY/J0jBVVLQEcMqLIT3QJjtwLym/vJU9fT1QHpHzna9uONXJi+r82CvWwwmlPVteI4nrpBaJEUB3s="],"cloud_instance_name":"microsoftonline.com","issuer":"https://login.microsoftonline.com/{tenantid}/v2.0"},{"kty":"RSA","use":"sig","kid":"imi0Y2z0dYKxBttAqK_Tt5hYBTk","x5t":"imi0Y2z0dYKxBttAqK_Tt5hYBTk","n":"vWNAUO6SsXegVgtOXEc6KVOMWEpSWc8dzo-iAYa886u7RRVKUukEypuJ5kpq8xzzeFI5h3s4JEPEsjMKZgVpjMMcSa2B4Q78K7kGCFdviMCqnsnYUQvz9Buwuw7u3gKSJhxGyNNqB6RfECWz3tM7l-0eogJtA2Yin5fFnZe3HZ7-nSYOwnIOREKOV-_In9VcXKJc_jCQJ1zeBtyjQ2i61kibtUABbUhMd0ytSnHOHVPu4nN1AMRpMcqKBVXL9uKaiSiNklYnVPHAyHIl8olg-gMOCQ8Ca8qoG2mH-skJnQz_MQPxWQSpwSpbE1k89uEq_sRmmji6EOrb0zSxvqev1Q","e":"AQAB","x5c":["MIIC/TCCAeWgAwIBAgIIZcqSu7KdqngwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNTAxMjcyMDE4MjFaFw0zMDAxMjcyMDE4MjFaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9Y0BQ7pKxd6BWC05cRzopU4xYSlJZzx3Oj6IBhrzzq7tFFUpS6QTKm4nmSmrzHPN4UjmHezgkQ8SyMwpmBWmMwxxJrYHhDvwruQYIV2+IwKqeydhRC/P0G7C7Du7eApImHEbI02oHpF8QJbPe0zuX7R6iAm0DZiKfl8Wdl7cdnv6dJg7Ccg5EQo5X78if1Vxcolz+MJAnXN4G3KNDaLrWSJu1QAFtSEx3TK1Kcc4dU+7ic3UAxGkxyooFVcv24pqJKI2SVidU8cDIciXyiWD6Aw4JDwJryqgbaYf6yQmdDP8xA/FZBKnBKlsTWTz24Sr+xGaaOLoQ6tvTNLG+p6/VAgMBAAGjITAfMB0GA1UdDgQWBBQ3jJNZaD5JanfQoBXsAkfd7fwqPjANBgkqhkiG9w0BAQsFAAOCAQEAOptwTJGTtUor1e7yQ9Cl+jMfQQrdKy5Sz+Qz+qqs5m59bTRSfCH79jIZOvkX1eW0O581buoCyvMDhTRB2U+YShptzMtQZPr8aTyupUjkFnP7hC7aPg3buuKBooQsU3L++ed7atoZjATmH0tWi2T403lAGIwhSK3KF6EPKOoktYSrLzIyvWY2vNUQzdME5bwekklph/y1uS2lunqqZtXvUj7DK4Ge4na2U3qvFsa84st/mBL6i3tRSvlLTAMpJoh3T7OgeFHvu+xMPbc/AWBxKVPfVZ/sl9cAZg0H3LLdn3WPVEEuQehFmCIXxJ5f8Lq9Y0OLoBL41tN0FsHYsQ2Oag=="],"cloud_instance_name":"microsoftonline.com","issuer":"https://login.microsoftonline.com/{tenantid}/v2.0"},{"kty":"RSA","use":"sig","kid":"7MQ8O1qILcYdNHYq_rZ6tkj1d20","x5t":"7MQ8O1qILcYdNHYq_rZ6tkj1d20","n":"m6UhWiyQfLhKQrXURCpzM5WPVCVyu4M8mD9ZmJ17BdoSO4QN0mhzSKL2G81Lq3hRaGvzog7ALPLAzokNI7BcCX3G_rczEYi4WOY2bBWehTQpuNtoUbZI_F-Xg0TWuDDnktLUx-cjcqBUQdMU9rHbuxCHSD3amMXeH06e7tI5_Hw2hiw-xZfW0X_oPLwqU-3nhDSFfG5vfR2VhwgFuKZvAXN0fjx3bT4pqKmRsE-Div4t8FdkR-DChXKx5eixiyZlbcE5LvF4y8C8vIyk4305HlRH57y4NjLu1Ra5b2nMCnIyUTk6JXkUIRMND-ljfI4pwDS4IukbOiLlRLKOX0Sk5Q","e":"AQAB","x5c":["MIIC6TCCAdGgAwIBAgIIBdyrYnfLPzQwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMB4XDTI1MDExNTE3MDE1M1oXDTMwMDExNTE3MDE1M1owIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm6UhWiyQfLhKQrXURCpzM5WPVCVyu4M8mD9ZmJ17BdoSO4QN0mhzSKL2G81Lq3hRaGvzog7ALPLAzokNI7BcCX3G/rczEYi4WOY2bBWehTQpuNtoUbZI/F+Xg0TWuDDnktLUx+cjcqBUQdMU9rHbuxCHSD3amMXeH06e7tI5/Hw2hiw+xZfW0X/oPLwqU+3nhDSFfG5vfR2VhwgFuKZvAXN0fjx3bT4pqKmRsE+Div4t8FdkR+DChXKx5eixiyZlbcE5LvF4y8C8vIyk4305HlRH57y4NjLu1Ra5b2nMCnIyUTk6JXkUIRMND+ljfI4pwDS4IukbOiLlRLKOX0Sk5QIDAQABoyEwHzAdBgNVHQ4EFgQUoRH/cFaPV6V0CUsQd6JC0mtpWaIwDQYJKoZIhvcNAQELBQADggEBAG65nK90vkQkkEMHROwBJeEy+U+q6ugMQ8hX/A0FiLzqJWAtstItg4ZGHH69iJSYtTvt+EKjxwhcDse3iKm2/jtlUmwfF0QX7nowUxprxCPSVduHkxzkOVmsD8/jKjIwvfSapUVzuvHmV4mx20+fqMKeEfTBn7E8ipHnQKJQAq04VI7w4yIHRjNafnRjqBWsMt+nNZ9JBujqdMb6xAktlMc4vuDa/5HzJaLCsp8jXJrbIoZ4QQt6hMLmh6bhdHQoX1STIUsxi3e2v1xzFRr70i38W09xTr4q1vkNeEYx3Qdwc8OW6kWGIaxtR9YNn+YePwInfmPxITLBbLeuI5NdCks="],"cloud_instance_name":"microsoftonline.us","issuer":"https://login.microsoftonline.com/{tenantid}/v2.0"},{"kty":"RSA","use":"sig","kid":"GP9ooSiPGgx444VrE6CWOt6_6xs","x5t":"GP9ooSiPGgx444VrE6CWOt6_6xs","n":"ozH9HJAkcEn23YeR0uYw2W0x087f7LzGfy5x20hJClLjLmgbKTZ9wHRxi1b7STJeQqjnLDrnwkYjTSYYAnIg99RA4uNAdRviTTJqpJwfiVTy0eIr18l4ksrtbDOtUSJUGURYPw-AZVV1W4aU9MLvAa6J2z2-S8ENqyy_i7xXLxUzYJSBtXTo9b1lHdEBzlqtxXoD-50k0qhY20YiyGHp7KXUyxVH6atu2ShHjO2TCl-8qp4xyXxwTp2ciUkZ1w0PiBKsfC2r-Ya0gQNXTdLwnXXkVNfCCz-G8uY2Ta2Y6R_Z_jKGo04pulkRhYa2SOnd4URB9DIBqrH86cI5JVkC3w","e":"AQAB","x5c":["MIIC6TCCAdGgAwIBAgIITnK9SyMdS0QwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMB4XDTI1MDIwMjE3MDE1M1oXDTMwMDIwMjE3MDE1M1owIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAozH9HJAkcEn23YeR0uYw2W0x087f7LzGfy5x20hJClLjLmgbKTZ9wHRxi1b7STJeQqjnLDrnwkYjTSYYAnIg99RA4uNAdRviTTJqpJwfiVTy0eIr18l4ksrtbDOtUSJUGURYPw+AZVV1W4aU9MLvAa6J2z2+S8ENqyy/i7xXLxUzYJSBtXTo9b1lHdEBzlqtxXoD+50k0qhY20YiyGHp7KXUyxVH6atu2ShHjO2TCl+8qp4xyXxwTp2ciUkZ1w0PiBKsfC2r+Ya0gQNXTdLwnXXkVNfCCz+G8uY2Ta2Y6R/Z/jKGo04pulkRhYa2SOnd4URB9DIBqrH86cI5JVkC3wIDAQABoyEwHzAdBgNVHQ4EFgQUAglPRZDkUCuL2sfZc2UlwUnGpEgwDQYJKoZIhvcNAQELBQADggEBAJUQKzWGldEvvrca+8kCyLq3MCbfQwtjljQ4WoY172O4oaLAgjOL3gGR1cSPSjlpgPUOLMyHD+w7aw1DRY+8f6m3rPyyiKQ+r0xCv521JpeZRDE2+pf4kdCjRJa7Hg8IJHyKsiUs/ejC30Bs1R43hWQJj+XWhBDdWagS6xzI5NTHN+VW9XSpn0u18HZaCvai4OFIcMBwQrowKvO8r4SqYBrqEAZhE7kOfF5V8mTAdfvVw4sa7t/ZrMy5kFtve9IR6LektWGJWxMu+/whMJVxkW7DuP8QA7e4AcMFEOAPe+6+RCzjyZN1wDdGRp0XExJVk9yq+Fodg/j/EGIPkAB0JgU="],"cloud_instance_name":"microsoftonline.us","issuer":"https://login.microsoftonline.com/{tenantid}/v2.0"},{"kty":"RSA","use":"sig","kid":"emt-c09x6CJ3y689a4FsJcHmZuU","x5t":"emt-c09x6CJ3y689a4FsJcHmZuU","n":"jPMLhViynsQ_CoY6GlimGuzreew-7OUzJG32-7ImZkRJ37iEATaaMzi6QQwEBKq8kssuncGd-lWvY16beGsSCDqdHiaCxQ3i3Rlu1bCZZhlvg0IDUfMRwA_iNvn66JtvQ7Gok3swXvjWXtmUH682nIzYjAiI-uBe42j9-NDssodabNA6LkDRg7IXyNc2Uu-Ovn_0tY3lQ4aE_O3rROHAHK-PB5rrAfkjMUkx5A_YSBCd-7rlVFzAbXGC1BPPrJC_3LpRIvpVHSNia4V6y5NBR2RAoL5e8d_II12Alivl_QMoEEJGwQacL4yOrnsz3dNPPaoRdeiyjkBg4zuqcMhVQw","e":"AQAB","x5c":["MIIDCzCCAfOgAwIBAgIRAMU22mI1SFqpuNL/vQh73XQwDQYJKoZIhvcNAQELBQAwKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MB4XDTI1MDIwMjIwMDIyNFoXDTMwMDIwMjIwMDIyNFowKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjPMLhViynsQ/CoY6GlimGuzreew+7OUzJG32+7ImZkRJ37iEATaaMzi6QQwEBKq8kssuncGd+lWvY16beGsSCDqdHiaCxQ3i3Rlu1bCZZhlvg0IDUfMRwA/iNvn66JtvQ7Gok3swXvjWXtmUH682nIzYjAiI+uBe42j9+NDssodabNA6LkDRg7IXyNc2Uu+Ovn/0tY3lQ4aE/O3rROHAHK+PB5rrAfkjMUkx5A/YSBCd+7rlVFzAbXGC1BPPrJC/3LpRIvpVHSNia4V6y5NBR2RAoL5e8d/II12Alivl/QMoEEJGwQacL4yOrnsz3dNPPaoRdeiyjkBg4zuqcMhVQwIDAQABoy4wLDAdBgNVHQ4EFgQUyeA2Psma0S3yH1QPTotuAxMp8DIwCwYDVR0PBAQDAgLEMA0GCSqGSIb3DQEBCwUAA4IBAQALpIzzUpYH8NGIYFLleQY8e3rBqTrzzRBsD2Jn/Un4U5ayJeZh0oeVOwBmA5HzrO+U/1o2e4OCOKLdyWZ0xkUJmvyNKH+1N8nhqKtst8bwRaT/tPHP7pvtTl9BHoGcaUZhiacDswmBVvPcbAavynMwBp4EXxVWihkIGGcSU7DdB5a4ygVrZmcEgDnDKl/avUG7/iKaCh6goEO4HaYEX5L3kjSH6XVDv7F82ke28tEFilxbPAqCP0MJ2xXFdnCQkzLFG94qghsKhHl7BlhCxYNG9rhsplLZNZJzFNQqmMO/LGSHhRrtQdr/FfSwMdT7glxddj24qwFtybAd/k6LnY+u"],"cloud_instance_name":"microsoftonline.com","issuer":"https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"},{"kty":"RSA","use":"sig","kid":"-cHvLoTRMfWXWuNxETS71oJ5vfc","x5t":"-cHvLoTRMfWXWuNxETS71oJ5vfc","n":"kzRkasC4SrApdvLeK-3vr_pNNOEqucf-REk5B9CpFLk7Rl6DLL7tT6HhYJkVwSXWJDhjUuy4vI1VvOLoFstMzPf8Gqh5-IC4_XvOCFtV0I_SBWMzJ5Smkd2tch5VxSe8jlRsOW1PQr2zj05GOXrHcmk4fF5ePDTgAmIIMPrTgwioBbFqhc6B6seJA42t0XtrwhrkvEbmxwjGFRgSXRH9fP_R6vBbb5ed8tYEOB0u2RM63xhwbnfAlTbp7TEtbwHnbWPomrj0h_GjSuW13cp0kxeswbyXOr4XggT9nn-d_ZqRYfADgekyrEZvF0DlmMmE4tGvSwbKpzWVdaxIyEs5SQ","e":"AQAB","x5c":["MIIDCzCCAfOgAwIBAgIRAJkNx0FMdQp1L9C/qHb6wX0wDQYJKoZIhvcNAQELBQAwKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MB4XDTI1MDExMDIxMDMzM1oXDTMwMDExMDIxMDMzM1owKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkzRkasC4SrApdvLeK+3vr/pNNOEqucf+REk5B9CpFLk7Rl6DLL7tT6HhYJkVwSXWJDhjUuy4vI1VvOLoFstMzPf8Gqh5+IC4/XvOCFtV0I/SBWMzJ5Smkd2tch5VxSe8jlRsOW1PQr2zj05GOXrHcmk4fF5ePDTgAmIIMPrTgwioBbFqhc6B6seJA42t0XtrwhrkvEbmxwjGFRgSXRH9fP/R6vBbb5ed8tYEOB0u2RM63xhwbnfAlTbp7TEtbwHnbWPomrj0h/GjSuW13cp0kxeswbyXOr4XggT9nn+d/ZqRYfADgekyrEZvF0DlmMmE4tGvSwbKpzWVdaxIyEs5SQIDAQABoy4wLDAdBgNVHQ4EFgQUCKY/k1b7uzbOsVB/djsRpWXs3HcwCwYDVR0PBAQDAgLEMA0GCSqGSIb3DQEBCwUAA4IBAQBXTDgF1liBJ8DzWqheOdIAOBu7U7RbG/OFCkVylL8pyM2267rD1+j7pxSqjcRibHpSsfs9iA1w3JmSU2X3RE8/EqIr4jm9OtJyZXcxCCDbWlRYC8D9fx79zeZwm1BCY6SNBaMOzPEuTk2ssNMpvMf87fTWteXuvqGN8BYqJIbJUtPe/YnFj9FGh6M15uOPfl4I41HsM6BGK6aWS97tp4C+hx/sCFJNhhQ2rGKp4rHlOP5Qfr+K2ZVTDQ7KRiFeeLi/Nu3rgtEm6XQLjtNl53Cz2cmK4yfwRstyviU+aMpLbD6Cc0hJ6K0ZvpPaiVEmInECFNZJGpwcrgAel07WA7Jc"],"cloud_instance_name":"microsoftonline.com","issuer":"https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"},{"kty":"RSA","use":"sig","kid":"aB0xDdGXk535PvewBP9Hl5pf7wc","x5t":"aB0xDdGXk535PvewBP9Hl5pf7wc","n":"oVqM3iwVBo1YghaFpKSJpMinrYMTBgDlAwfPuuGepQDrXG0My4C7ZvQ1srMlHKQG5ruTO2_cNFepuPyg1CrM5gVXbLutQ_Q_vDLkBpBzLlbEHEZ9pDu12HplHDmXgITHxEskMzZ115LFzt9Tc7ftHRtt6lwNj8CBbOwFnlOTXSzWwbrNdurwAkXwymJ0tz8ABYUCVBmIupuX-g1XdBu3BI7C6pWsqdQS_81WM-6qk_OZqEv3C0SIRKXYGBNbY5AdqG4gLrOqmnoXKnOo13kPY0bjx7UAD18wtCshiJY2elC_3ljVhtGu0U86TfQUDY5tGgrQPQy-udoPsgKXsv_mVQ","e":"AQAB","x5c":["MIIDCzCCAfOgAwIBAgIRAOM3wRp7X25WmIvQTAndPGMwDQYJKoZIhvcNAQELBQAwKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MB4XDTI0MTIyOTAwMDUyN1oXDTI5MTIyOTAwMDUyN1owKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoVqM3iwVBo1YghaFpKSJpMinrYMTBgDlAwfPuuGepQDrXG0My4C7ZvQ1srMlHKQG5ruTO2/cNFepuPyg1CrM5gVXbLutQ/Q/vDLkBpBzLlbEHEZ9pDu12HplHDmXgITHxEskMzZ115LFzt9Tc7ftHRtt6lwNj8CBbOwFnlOTXSzWwbrNdurwAkXwymJ0tz8ABYUCVBmIupuX+g1XdBu3BI7C6pWsqdQS/81WM+6qk/OZqEv3C0SIRKXYGBNbY5AdqG4gLrOqmnoXKnOo13kPY0bjx7UAD18wtCshiJY2elC/3ljVhtGu0U86TfQUDY5tGgrQPQy+udoPsgKXsv/mVQIDAQABoy4wLDAdBgNVHQ4EFgQUt9KYDuU6tZfVe7SsO9zs3DNT3YgwCwYDVR0PBAQDAgLEMA0GCSqGSIb3DQEBCwUAA4IBAQB4RUzhNlS3VQVAL0x8PC3u0/ge2LTlcbL1x2hjoeacCRAxDIPtJbAY6KUquiKzDkZwHBRTjnGLpQHYQjffv7qF3KrAgc29tc/jIq9dpZ+rir4yjrCGcXsRyCd7CfGzEAZHmfJCFdOsJSWIZiDsjOM5PaML1kUl45iRU6VQLad7RqprdddCsLuXhciXsMT6sJs1opUlLmVi9pF02HMd/63yWrFY3Px1qfDC8W5aQFA6IzTVbKNgBkBS45KebdZ0oYY/0wozNPZOtctpc2cMoIpSxNGQ6SpF2JEdVntoWyjBgxfn3ddwQfTHjZ4/BSvTTC954Ft6XhcENdRCH8iuy6kz"],"cloud_instance_name":"microsoftonline.com","issuer":"https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"},{"kty":"RSA","use":"sig","kid":"yef4qNtpIsfj5moDaHCqP5zlBAc","x5t":"yef4qNtpIsfj5moDaHCqP5zlBAc","n":"mw709TPFFWktSM2H-BpZqDCImsFudwpYjabyPurErS2d1polqDAyx6Qaic2J9XyqThhSjRVK5rYVqcaK8yjqOW7neKV9xC2oGcmA8WS6WwlvTuwsPAVPV1Gwna9DALBz-bhZvsQ-e-WZJp1d1aQ1PVubc9lveDMKlMu05abf9mtlvoS3kUr3EPNg22B82kgyGXR-jQAsMiqE-W_HTlLNu9u79qBZfFiQ2YNzDevEk8ARx35YIBh5lL0HbahAWp1X2eyVWFW-fscapwQHd78p5PYp-QLLYpuwDX7_-FdQwoe8-2jrKavYlwUKEGw6tT0H9X0rNLLNiwFDozZ9mJxTww","e":"AQAB","x5c":["MIIDCzCCAfOgAwIBAgIRAPciYzAougxYP/KWCOrEcH8wDQYJKoZIhvcNAQELBQAwKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MB4XDTI0MTIxNjAyMzIzM1oXDTI5MTIxNjAyMzIzM1owKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmw709TPFFWktSM2H+BpZqDCImsFudwpYjabyPurErS2d1polqDAyx6Qaic2J9XyqThhSjRVK5rYVqcaK8yjqOW7neKV9xC2oGcmA8WS6WwlvTuwsPAVPV1Gwna9DALBz+bhZvsQ+e+WZJp1d1aQ1PVubc9lveDMKlMu05abf9mtlvoS3kUr3EPNg22B82kgyGXR+jQAsMiqE+W/HTlLNu9u79qBZfFiQ2YNzDevEk8ARx35YIBh5lL0HbahAWp1X2eyVWFW+fscapwQHd78p5PYp+QLLYpuwDX7/+FdQwoe8+2jrKavYlwUKEGw6tT0H9X0rNLLNiwFDozZ9mJxTwwIDAQABoy4wLDAdBgNVHQ4EFgQUWMjkNWDYipJUZfCQthYfEkOYxDUwCwYDVR0PBAQDAgLEMA0GCSqGSIb3DQEBCwUAA4IBAQBV+UPkfa6ClkBQqyOAcQN4YVPoB8LFpNUYXWho2+ZZWOMlwJ4VHDqvf+rnj/LTxb+TkXqfVM2gEpzuMSNXJPQXszImReNjiA68neUB3fgyWGJc76jhccjUQim184c1/SdFPShPFk2BhZB84dJt3PfclZLm1pBjBegFYZfiK/MpqV/l+c7VVj/LYrhiYKDo5E3KC0MK+PWQ7IbWMZ2Ws/2cQdP7dajizzEt8MqxzyTvylXF8Q+l3na5+l4c+ycHGc7RANgdpVdlKAqj1K5hshyqTvZ4DVMoWVaHJHl2knPNFjt3W36mQDn38pfLSnjMNfrfVeUkgMa16MrWA083Q1ew"],"cloud_instance_name":"microsoftonline.com","issuer":"https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"}]} \ No newline at end of file diff --git a/rust/gel-jwt/src/testcases/jwkset-prv.json b/rust/gel-jwt/src/testcases/jwkset-prv.json new file mode 100644 index 000000000000..315f3ad582ba --- /dev/null +++ b/rust/gel-jwt/src/testcases/jwkset-prv.json @@ -0,0 +1,29 @@ +{ + "keys": [ + { + "crv": "P-256", + "d": "iCum_2Siw6gU9opXbUSFpVHsd-1mkRA6NkEeJyryluU", + "kid": "kid-2", + "kty": "EC", + "x": "ThNnXqdOA8pFnW047DI2WtEv5AJJTGbejStYBKIp--w", + "y": "Y0EgzY-exTDOeQGkmWvVxHwdzSvQc4WSuh0y_j_cuX0" + }, + { + "k": "vwGPSUI3FS7ME6yBJjNaYw", + "kid": "kid-3", + "kty": "oct" + }, + { + "d": "GVmhWyQm9VEokPs78XH5xIAygScaWMzNdifuU-2sU2DKjlynzN68fP4VJw68CcA7xXHbKrnwWB557O7X-Ya17pUCjWGcpvgYI-dDVjLriVnSiGNzNPjiwPDvzVemZxv8QqJ9OHKPxNXHSI_i87DX-UVB3XeMwXK9r6AWXbDoOLdnI8PmBkTjZW7-xq2yslM7Wgb9k-3z2AYpm2I1JwY5140wP6tlAJUeELlZ7uldH0LoAgp7d3BiVKuA8gZbjiz5Z05YzYztKPRDVf8R0NnjAJUaO7x0m8akgyB4oz2hrtjGw6sIkTBuT6swGPam5T7qkEQwspiSZlsDMqzOsYf3EQ", + "dp": "clKDZKgMHsNcHozw04WCwO2p6GCVWqtlbiaqsTnG_MMYtx9kyrq2euOiDvVp0oN3I31AYJKAwbqXpekYFlvpiJeGOUeSWuOFQ4p3DEKS7FHGwag_a9OccinXyUkFoDuZtN36RQPoh_R-b7R73PaPYIW8EP8h79nPkvpjnV72efk", + "dq": "p65ytqK5yKaNmCKvjksc_L_a7pEZy0aA7LkGAPRFT-u0OwzZKivwV-70AkG7obYTRCRHa1Tbk8E7zbUtJVpUJGyWY428jO--VV5UwAW8dHKxVa53JSukCdZdP7RW5P4g_xzqi6THjm_XV9C_IFQfQUgX_gdTkS7dlpokxgDttTE", + "e": "AQAB", + "kid": "kid-1", + "kty": "RSA", + "n": "urva9iyLbBs3c3aaa2GbTjwbxtgTkDfi2dt4pO5edzaGO-JeIlBRXUWqWDeQmh63c779Kwxuql06bCdje0H2bUCSeDiva-TtU2Xdr6vM59cjT7tG0weAhv9cDiQTL2bKWtTdLpQTbwZcxr7BhSFW0Ey-VJP2A_TPlZaSn-dPQsed16OEQYCtnD9TtF-yPPKk22UWrs6XQl_bAA_t1pkjm7pD-LfSKIgb46e993q0kMAPoh3kaCDXhs_RB2RTo22dZTTbOcRmiCdqR7LnVB6WFSB9JMnObbBBcf_knL-puISKWHaV4BLHh28_kQmKIMXKaEdoZ31OlNbn3-2XglpMGQ", + "p": "7v17ivZSVbzE-fMXxDZWg0cl23hMIekrRoFBmo0TRp5-yPqxBU3D6Yt25l8GQSQ4kIoAiZoiKvrVhncbe1qGpJmS8XaMki8zMTzsFMmG6E7wqO1oLka3-aY5WZYQPnZrwriCSVCLX7qLWBF4r-FENc5KfrpNw3CaRqrJLCIRFzU", + "q": "yAY8hjj_n9OrYAIflAVYjQqkH5epzHJ_UOs7Ex11nCO7EafcBenegYjOciG50sp5iI_xdcv0yE_YaCgWCuHn4D5NkvvhobWJJbo6LoiHgx4z5DC-Wnv9MUXVkdqRPM24-XK6UC4Dx6vuEXIxeLyerRyKn-DE79oL16CyyASQqdU", + "qi": "GfjEBT4CZZc4FjqmWW6HjEdkIveZLeX8FimehtGMzIQXVPlvn1iNPLyFK3OhvCKiDRP6xl4Q14JbLCIXvd8t2PDJKowVVzDBhjnHsqQSw5HuwG7aWr9Akj0kjb2-dBxFSW8PdrYx_A244LEggF2Yrb6Z9Y8AkHxz1ccSrk_Yotw" + } + ] +} diff --git a/rust/gel-jwt/src/testcases/jwkset-pub.json b/rust/gel-jwt/src/testcases/jwkset-pub.json new file mode 100644 index 000000000000..28f75db6aaed --- /dev/null +++ b/rust/gel-jwt/src/testcases/jwkset-pub.json @@ -0,0 +1,17 @@ +{ + "keys": [ + { + "e": "AQAB", + "kid": "kid-1", + "kty": "RSA", + "n": "urva9iyLbBs3c3aaa2GbTjwbxtgTkDfi2dt4pO5edzaGO-JeIlBRXUWqWDeQmh63c779Kwxuql06bCdje0H2bUCSeDiva-TtU2Xdr6vM59cjT7tG0weAhv9cDiQTL2bKWtTdLpQTbwZcxr7BhSFW0Ey-VJP2A_TPlZaSn-dPQsed16OEQYCtnD9TtF-yPPKk22UWrs6XQl_bAA_t1pkjm7pD-LfSKIgb46e993q0kMAPoh3kaCDXhs_RB2RTo22dZTTbOcRmiCdqR7LnVB6WFSB9JMnObbBBcf_knL-puISKWHaV4BLHh28_kQmKIMXKaEdoZ31OlNbn3-2XglpMGQ" + }, + { + "crv": "P-256", + "kid": "kid-2", + "kty": "EC", + "x": "ThNnXqdOA8pFnW047DI2WtEv5AJJTGbejStYBKIp--w", + "y": "Y0EgzY-exTDOeQGkmWvVxHwdzSvQc4WSuh0y_j_cuX0" + } + ] +} \ No newline at end of file diff --git a/rust/gel-jwt/src/testcases/jwkset-slck.json b/rust/gel-jwt/src/testcases/jwkset-slck.json new file mode 100644 index 000000000000..20803ce6cfeb --- /dev/null +++ b/rust/gel-jwt/src/testcases/jwkset-slck.json @@ -0,0 +1 @@ +{"keys":[{"e":"AQAB","n":"zQqzXfb677bpMKw0idKC5WkVLyqk04PWMsWYJDKqMUUuu_PmzdsvXBfHU7tcZiNoHDuVvGDqjqnkLPEzjXnaZY0DDDHvJKS0JI8fkxIfV1kNy3DkpQMMhgAwnftUiSXgb5clypOmotAEm59gHPYjK9JHBWoHS14NYEYZv9NVy0EkjauyYDSTz589aiKU5lA-cePG93JnqLw8A82kfTlrJ1IIJo2isyBGANr0YzR-d3b_5EvP7ivU7Ph2v5JcEUHeiLSRzIzP3PuyVFrPH659Deh-UAsDFOyJbIcimg9ITnk5_45sb_Xcd_UN6h5I7TGOAFaJN4oi4aaGD4elNi_K1Q","kty":"RSA","kid":"mB2MAyKSn555isd0EbdhKx6nkyAi9xLq8rvCEb_nOyY","alg":"RS256"}]} \ No newline at end of file diff --git a/rust/gel-jwt/src/testcases/prime256v1-prv-pkcs8-asn1.txt b/rust/gel-jwt/src/testcases/prime256v1-prv-pkcs8-asn1.txt new file mode 100644 index 000000000000..318eccd190dd --- /dev/null +++ b/rust/gel-jwt/src/testcases/prime256v1-prv-pkcs8-asn1.txt @@ -0,0 +1,26 @@ + 0:d=0 hl=3 l= 135 cons: SEQUENCE + 3:d=1 hl=2 l= 1 prim: INTEGER :00 + 6:d=1 hl=2 l= 19 cons: SEQUENCE + 8:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey + 17:d=2 hl=2 l= 8 prim: OBJECT :prime256v1 + 27:d=1 hl=2 l= 109 prim: OCTET STRING + 0000 - 30 6b 02 01 01 04 20 28-30 0f f1 58 33 bd 38 f2 0k.... (0..X3.8. + 0010 - 03 71 48 d0 63 19 bf 83-cd 13 65 c1 de fa cf e3 .qH.c.....e..... + 0020 - f3 71 ee 12 47 a1 ff a1-44 03 42 00 04 c1 cf 62 .q..G...D.B....b + 0030 - 6f 1c 6d 3d 02 70 13 6b-76 5d 3d 89 da bb 66 e8 o.m=.p.kv]=...f. + 0040 - 95 fe ae fa e3 54 14 01-89 57 c4 f5 c0 56 14 cc .....T...W...V.. + 0050 - 43 ec cd 8d d4 93 24 14-7b 7b 83 a7 f8 83 12 6f C.....$.{{.....o + 0060 - 80 3b eb a2 eb 43 97 3c-95 89 a1 68 21 .;...C.<...h! +-- OCTET STRING at offset 27 -- + 0:d=0 hl=2 l= 107 cons: SEQUENCE + 2:d=1 hl=2 l= 1 prim: INTEGER :01 + 5:d=1 hl=2 l= 32 prim: OCTET STRING + 0000 - 28 30 0f f1 58 33 bd 38-f2 03 71 48 d0 63 19 bf (0..X3.8..qH.c.. + 0010 - 83 cd 13 65 c1 de fa cf-e3 f3 71 ee 12 47 a1 ff ...e......q..G.. + 39:d=1 hl=2 l= 68 cons: cont [ 1 ] + 41:d=2 hl=2 l= 66 prim: BIT STRING + 0000 - 00 04 c1 cf 62 6f 1c 6d-3d 02 70 13 6b 76 5d 3d ....bo.m=.p.kv]= + 0010 - 89 da bb 66 e8 95 fe ae-fa e3 54 14 01 89 57 c4 ...f......T...W. + 0020 - f5 c0 56 14 cc 43 ec cd-8d d4 93 24 14 7b 7b 83 ..V..C.....$.{{. + 0030 - a7 f8 83 12 6f 80 3b eb-a2 eb 43 97 3c 95 89 a1 ....o.;...C.<... + 0040 - 68 21 h! diff --git a/rust/gel-jwt/src/testcases/prime256v1-prv-pkcs8.pem b/rust/gel-jwt/src/testcases/prime256v1-prv-pkcs8.pem new file mode 100644 index 000000000000..29b45f412bd4 --- /dev/null +++ b/rust/gel-jwt/src/testcases/prime256v1-prv-pkcs8.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKDAP8VgzvTjyA3FI +0GMZv4PNE2XB3vrP4/Nx7hJHof+hRANCAATBz2JvHG09AnATa3ZdPYnau2bolf6u ++uNUFAGJV8T1wFYUzEPszY3UkyQUe3uDp/iDEm+AO+ui60OXPJWJoWgh +-----END PRIVATE KEY----- diff --git a/rust/gel-jwt/src/testcases/prime256v1-prv-sec1-asn1.txt b/rust/gel-jwt/src/testcases/prime256v1-prv-sec1-asn1.txt new file mode 100644 index 000000000000..d3aa8f041e03 --- /dev/null +++ b/rust/gel-jwt/src/testcases/prime256v1-prv-sec1-asn1.txt @@ -0,0 +1,18 @@ + 0:d=0 hl=2 l= 119 cons: SEQUENCE + 2:d=1 hl=2 l= 1 prim: INTEGER :01 + 5:d=1 hl=2 l= 32 prim: OCTET STRING + 0000 - 28 30 0f f1 58 33 bd 38-f2 03 71 48 d0 63 19 bf (0..X3.8..qH.c.. + 0010 - 83 cd 13 65 c1 de fa cf-e3 f3 71 ee 12 47 a1 ff ...e......q..G.. + 39:d=1 hl=2 l= 10 cons: cont [ 0 ] + 41:d=2 hl=2 l= 8 prim: OBJECT :prime256v1 + 51:d=1 hl=2 l= 68 cons: cont [ 1 ] + 53:d=2 hl=2 l= 66 prim: BIT STRING + 0000 - 00 04 c1 cf 62 6f 1c 6d-3d 02 70 13 6b 76 5d 3d ....bo.m=.p.kv]= + 0010 - 89 da bb 66 e8 95 fe ae-fa e3 54 14 01 89 57 c4 ...f......T...W. + 0020 - f5 c0 56 14 cc 43 ec cd-8d d4 93 24 14 7b 7b 83 ..V..C.....$.{{. + 0030 - a7 f8 83 12 6f 80 3b eb-a2 eb 43 97 3c 95 89 a1 ....o.;...C.<... + 0040 - 68 21 h! +-- OCTET STRING at offset 5 -- +Error in encoding +-- BITSTRING at offset 53 -- +Error in encoding diff --git a/rust/gel-jwt/src/testcases/prime256v1-prv-sec1.pem b/rust/gel-jwt/src/testcases/prime256v1-prv-sec1.pem new file mode 100644 index 000000000000..19357441cb63 --- /dev/null +++ b/rust/gel-jwt/src/testcases/prime256v1-prv-sec1.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICgwD/FYM7048gNxSNBjGb+DzRNlwd76z+Pzce4SR6H/oAoGCCqGSM49 +AwEHoUQDQgAEwc9ibxxtPQJwE2t2XT2J2rtm6JX+rvrjVBQBiVfE9cBWFMxD7M2N +1JMkFHt7g6f4gxJvgDvroutDlzyViaFoIQ== +-----END EC PRIVATE KEY----- diff --git a/rust/gel-jwt/src/testcases/prime256v1-pub-spki-asn1.txt b/rust/gel-jwt/src/testcases/prime256v1-pub-spki-asn1.txt new file mode 100644 index 000000000000..35fbfd6c59bf --- /dev/null +++ b/rust/gel-jwt/src/testcases/prime256v1-pub-spki-asn1.txt @@ -0,0 +1,10 @@ + 0:d=0 hl=2 l= 57 cons: SEQUENCE + 2:d=1 hl=2 l= 19 cons: SEQUENCE + 4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey + 13:d=2 hl=2 l= 8 prim: OBJECT :prime256v1 + 23:d=1 hl=2 l= 34 prim: BIT STRING + 0000 - 00 03 c1 cf 62 6f 1c 6d-3d 02 70 13 6b 76 5d 3d ....bo.m=.p.kv]= + 0010 - 89 da bb 66 e8 95 fe ae-fa e3 54 14 01 89 57 c4 ...f......T...W. + 0020 - f5 c0 .. +-- BITSTRING at offset 23 -- +Error in encoding diff --git a/rust/gel-jwt/src/testcases/prime256v1-pub-spki-uncompressed-asn1.txt b/rust/gel-jwt/src/testcases/prime256v1-pub-spki-uncompressed-asn1.txt new file mode 100644 index 000000000000..a5dac362b1d3 --- /dev/null +++ b/rust/gel-jwt/src/testcases/prime256v1-pub-spki-uncompressed-asn1.txt @@ -0,0 +1,12 @@ + 0:d=0 hl=2 l= 89 cons: SEQUENCE + 2:d=1 hl=2 l= 19 cons: SEQUENCE + 4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey + 13:d=2 hl=2 l= 8 prim: OBJECT :prime256v1 + 23:d=1 hl=2 l= 66 prim: BIT STRING + 0000 - 00 04 c1 cf 62 6f 1c 6d-3d 02 70 13 6b 76 5d 3d ....bo.m=.p.kv]= + 0010 - 89 da bb 66 e8 95 fe ae-fa e3 54 14 01 89 57 c4 ...f......T...W. + 0020 - f5 c0 56 14 cc 43 ec cd-8d d4 93 24 14 7b 7b 83 ..V..C.....$.{{. + 0030 - a7 f8 83 12 6f 80 3b eb-a2 eb 43 97 3c 95 89 a1 ....o.;...C.<... + 0040 - 68 21 h! +-- BITSTRING at offset 23 -- +Error in encoding diff --git a/rust/gel-jwt/src/testcases/prime256v1-pub-spki-uncompressed.pem b/rust/gel-jwt/src/testcases/prime256v1-pub-spki-uncompressed.pem new file mode 100644 index 000000000000..560aa1679ca1 --- /dev/null +++ b/rust/gel-jwt/src/testcases/prime256v1-pub-spki-uncompressed.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwc9ibxxtPQJwE2t2XT2J2rtm6JX+ +rvrjVBQBiVfE9cBWFMxD7M2N1JMkFHt7g6f4gxJvgDvroutDlzyViaFoIQ== +-----END PUBLIC KEY----- diff --git a/rust/gel-jwt/src/testcases/prime256v1-pub-spki.pem b/rust/gel-jwt/src/testcases/prime256v1-pub-spki.pem new file mode 100644 index 000000000000..d369449dd571 --- /dev/null +++ b/rust/gel-jwt/src/testcases/prime256v1-pub-spki.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgADwc9ibxxtPQJwE2t2XT2J2rtm6JX+ +rvrjVBQBiVfE9cA= +-----END PUBLIC KEY----- diff --git a/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs1-asn1.txt b/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs1-asn1.txt new file mode 100644 index 000000000000..7402e900a900 --- /dev/null +++ b/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs1-asn1.txt @@ -0,0 +1,10 @@ + 0:d=0 hl=4 l=1185 cons: SEQUENCE + 4:d=1 hl=2 l= 1 prim: INTEGER :00 + 7:d=1 hl=4 l= 257 prim: INTEGER :A15F236FF920DD14F22E7AEDB2ADBF6FF94898B9DD69CC0C85141146495F47B32CD59DEC10C581C9D31EE879B64A5A2514E1982243E60229B8DE48221C24062D9E3E41F144CFFAA22DF12C132D219539096EA5B443FC7E187BDEA581B0A8A96B202DB63EE4E89C54354555977A78D33779CB6F070FB8FB40827C1EA50468DEFD94A4A82328B64BACA7EB9777A56FB988A774CEEBFE7617855C171602D35B2A7B738B9B3D651D9E6803177CD037C92990467648BF7094F03BA51CB3065FB5FD13DD3BB5D862A0F6F713C5E02A07A448C9D627BA57724176230B40FA5634BD2507E4B70D65396AFCBBE1303A57846B4AFE3A5B097BE2FE28FCEDDA13FED9CDAA2F + 268:d=1 hl=2 l= 3 prim: INTEGER :010001 + 273:d=1 hl=3 l= 255 prim: INTEGER :455F0FC5BF069FDCF25CF7D360D949323EF6C95BBE6E21F8424E66B36DC3D9B5E77431DC01C9E3E32752FED58E62266AB21DF62CB14A668B425F4EB431BA179D6032CEAC698840E27279A046F103D08A20FF0861AD2185493E2C8D540D952397898A9711074F98F5D74AF375A639375B9BC953B359FF1BCE22146579A371BFE1ACD5DB4AD063887BA3DC3F6886FA37071AAB9D095AC8F7A6E37FA06A786B754887192F877B85B25F19722AC5BEF8BA35E7BEAB75E860958E9901326D3144906DC855EAB9AD7645CF349C9E6B4727A73C586FD762655F2E3D594724991104E3335F338CF0E4DE662855F90C45A98CF7C911461547C820293BE1273BEA8928A5 + 531:d=1 hl=3 l= 129 prim: INTEGER :D0DA5C4ADEB7A4A88EB3B040722CA93B457D2977A312F8960776ABE78BD4B8DE47F0D40C317F377880C10EFF59FC8EAEBA1BD4050CF952C19BE1C24F32D666B8A8CC8B08C2D125E0C983BA38A9F1ABCF0F3A0E7AF0D2E3B27F1949350E611FCA109FB50C81D7CDA6B1B0911E8AB8265F12BF14D71713A18148028E013A13FCED + 663:d=1 hl=3 l= 129 prim: INTEGER :C5CCD354600C7D16BF7B9427E50EFB33214B14D9C649043DF7F33668F270007FF2EC07A1135A86BAE1593B5C9AF540C45CAEA1F43537D1BC518A8D484EEA3BC6C9560BC0E4C5F9657103021A80986BCC283423F99873C14D46CB654C20F29333CF476EF7861249434E745DF5EF93EC9F07317739DFEB0F9FE2CB8D077F4B7C0B + 795:d=1 hl=3 l= 128 prim: INTEGER :34D98CD1455AA3482414445D8A86D2AC35015F24EF1735E08132FE7D315D3B0AC499A48F115767EA0E6ACC28C1D4AF2677E1E2DD045373259B149DDEFCB6547815FEEC8FE2FC99E1301D2D5A7966B65B473721C2EA7DF33090090E0567061CCD3D37ACD0E56A7E97D80F1E29E460851539E1309CDD3212846C7C7902C6779861 + 926:d=1 hl=3 l= 129 prim: INTEGER :B38A52FC905E65A0A35869C7B89BDB99B28BB336654C5BA4600F0C81402637DDEBC320BDAD928B1ED073AD7546567D5E7F7E281541C514046AC367B08DA9016F53CB5DC9CE1E815CF9B2B0531C0CBE3446FDE4F5A6D2D34FF085A8C1EC5A231171013711484A0AE4242F6E26513BA519486F36F87A6EA3F50DE8936F8DB517B7 + 1058:d=1 hl=3 l= 128 prim: INTEGER :6C2F207FB25D8CB6FD7C0497E714CC5202CF7514DF4D81FEF622F483DEA5AC812A4A675115A24C32D3832645F0EB3F88A987495EFBCCC349227178E04673B241F431E32649D607D7DA03C932713F1923890160E4232810E68274E3C4FF028E21D5D0AED08084C555A77B3512F16EAA5332B2BAC70F6A1712C594E09A4DB4DCF4 diff --git a/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs1.pem b/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs1.pem new file mode 100644 index 000000000000..96d421e3df60 --- /dev/null +++ b/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs1.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEAoV8jb/kg3RTyLnrtsq2/b/lImLndacwMhRQRRklfR7Ms1Z3s +EMWBydMe6Hm2SlolFOGYIkPmAim43kgiHCQGLZ4+QfFEz/qiLfEsEy0hlTkJbqW0 +Q/x+GHvepYGwqKlrIC22PuTonFQ1RVWXenjTN3nLbwcPuPtAgnwepQRo3v2UpKgj +KLZLrKfrl3elb7mIp3TO6/52F4VcFxYC01sqe3OLmz1lHZ5oAxd80DfJKZBGdki/ +cJTwO6UcswZftf0T3Tu12GKg9vcTxeAqB6RIydYnuldyQXYjC0D6VjS9JQfktw1l +OWr8u+EwOleEa0r+OlsJe+L+KPzt2hP+2c2qLwIDAQABAoH/RV8Pxb8Gn9zyXPfT +YNlJMj72yVu+biH4Qk5ms23D2bXndDHcAcnj4ydS/tWOYiZqsh32LLFKZotCX060 +MboXnWAyzqxpiEDicnmgRvED0Iog/whhrSGFST4sjVQNlSOXiYqXEQdPmPXXSvN1 +pjk3W5vJU7NZ/xvOIhRleaNxv+Gs1dtK0GOIe6PcP2iG+jcHGqudCVrI96bjf6Bq +eGt1SIcZL4d7hbJfGXIqxb74ujXnvqt16GCVjpkBMm0xRJBtyFXqua12Rc80nJ5r +RyenPFhv12JlXy49WUckmREE4zNfM4zw5N5mKFX5DEWpjPfJEUYVR8ggKTvhJzvq +iSilAoGBANDaXEret6SojrOwQHIsqTtFfSl3oxL4lgd2q+eL1LjeR/DUDDF/N3iA +wQ7/WfyOrrob1AUM+VLBm+HCTzLWZriozIsIwtEl4MmDujip8avPDzoOevDS47J/ +GUk1DmEfyhCftQyB182msbCRHoq4Jl8SvxTXFxOhgUgCjgE6E/ztAoGBAMXM01Rg +DH0Wv3uUJ+UO+zMhSxTZxkkEPffzNmjycAB/8uwHoRNahrrhWTtcmvVAxFyuofQ1 +N9G8UYqNSE7qO8bJVgvA5MX5ZXEDAhqAmGvMKDQj+ZhzwU1Gy2VMIPKTM89HbveG +EklDTnRd9e+T7J8HMXc53+sPn+LLjQd/S3wLAoGANNmM0UVao0gkFERdiobSrDUB +XyTvFzXggTL+fTFdOwrEmaSPEVdn6g5qzCjB1K8md+Hi3QRTcyWbFJ3e/LZUeBX+ +7I/i/JnhMB0tWnlmtltHNyHC6n3zMJAJDgVnBhzNPTes0OVqfpfYDx4p5GCFFTnh +MJzdMhKEbHx5AsZ3mGECgYEAs4pS/JBeZaCjWGnHuJvbmbKLszZlTFukYA8MgUAm +N93rwyC9rZKLHtBzrXVGVn1ef34oFUHFFARqw2ewjakBb1PLXcnOHoFc+bKwUxwM +vjRG/eT1ptLTT/CFqMHsWiMRcQE3EUhKCuQkL24mUTulGUhvNvh6bqP1DeiTb421 +F7cCgYBsLyB/sl2Mtv18BJfnFMxSAs91FN9Ngf72IvSD3qWsgSpKZ1EVokwy04Mm +RfDrP4iph0le+8zDSSJxeOBGc7JB9DHjJknWB9faA8kycT8ZI4kBYOQjKBDmgnTj +xP8CjiHV0K7QgITFVad7NRLxbqpTMrK6xw9qFxLFlOCaTbTc9A== +-----END RSA PRIVATE KEY----- diff --git a/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs8-asn1.txt b/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs8-asn1.txt new file mode 100644 index 000000000000..49a18751c76c --- /dev/null +++ b/rust/gel-jwt/src/testcases/rsa2048-prv-pkcs8-asn1.txt @@ -0,0 +1,92 @@ + 0:d=0 hl=4 l=1211 cons: SEQUENCE + 4:d=1 hl=2 l= 1 prim: INTEGER :00 + 7:d=1 hl=2 l= 13 cons: SEQUENCE + 9:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption + 20:d=2 hl=2 l= 0 prim: NULL + 22:d=1 hl=4 l=1189 prim: OCTET STRING + 0000 - 30 82 04 a1 02 01 00 02-82 01 01 00 a1 5f 23 6f 0............_#o + 0010 - f9 20 dd 14 f2 2e 7a ed-b2 ad bf 6f f9 48 98 b9 . ....z....o.H.. + 0020 - dd 69 cc 0c 85 14 11 46-49 5f 47 b3 2c d5 9d ec .i.....FI_G.,... + 0030 - 10 c5 81 c9 d3 1e e8 79-b6 4a 5a 25 14 e1 98 22 .......y.JZ%..." + 0040 - 43 e6 02 29 b8 de 48 22-1c 24 06 2d 9e 3e 41 f1 C..)..H".$.-.>A. + 0050 - 44 cf fa a2 2d f1 2c 13-2d 21 95 39 09 6e a5 b4 D...-.,.-!.9.n.. + 0060 - 43 fc 7e 18 7b de a5 81-b0 a8 a9 6b 20 2d b6 3e C.~.{......k -.> + 0070 - e4 e8 9c 54 35 45 55 97-7a 78 d3 37 79 cb 6f 07 ...T5EU.zx.7y.o. + 0080 - 0f b8 fb 40 82 7c 1e a5-04 68 de fd 94 a4 a8 23 ...@.|...h.....# + 0090 - 28 b6 4b ac a7 eb 97 77-a5 6f b9 88 a7 74 ce eb (.K....w.o...t.. + 00a0 - fe 76 17 85 5c 17 16 02-d3 5b 2a 7b 73 8b 9b 3d .v..\....[*{s..= + 00b0 - 65 1d 9e 68 03 17 7c d0-37 c9 29 90 46 76 48 bf e..h..|.7.).FvH. + 00c0 - 70 94 f0 3b a5 1c b3 06-5f b5 fd 13 dd 3b b5 d8 p..;...._....;.. + 00d0 - 62 a0 f6 f7 13 c5 e0 2a-07 a4 48 c9 d6 27 ba 57 b......*..H..'.W + 00e0 - 72 41 76 23 0b 40 fa 56-34 bd 25 07 e4 b7 0d 65 rAv#.@.V4.%....e + 00f0 - 39 6a fc bb e1 30 3a 57-84 6b 4a fe 3a 5b 09 7b 9j...0:W.kJ.:[.{ + 0100 - e2 fe 28 fc ed da 13 fe-d9 cd aa 2f 02 03 01 00 ..(......../.... + 0110 - 01 02 81 ff 45 5f 0f c5-bf 06 9f dc f2 5c f7 d3 ....E_.......\.. + 0120 - 60 d9 49 32 3e f6 c9 5b-be 6e 21 f8 42 4e 66 b3 `.I2>..[.n!.BNf. + 0130 - 6d c3 d9 b5 e7 74 31 dc-01 c9 e3 e3 27 52 fe d5 m....t1.....'R.. + 0140 - 8e 62 26 6a b2 1d f6 2c-b1 4a 66 8b 42 5f 4e b4 .b&j...,.Jf.B_N. + 0150 - 31 ba 17 9d 60 32 ce ac-69 88 40 e2 72 79 a0 46 1...`2..i.@.ry.F + 0160 - f1 03 d0 8a 20 ff 08 61-ad 21 85 49 3e 2c 8d 54 .... ..a.!.I>,.T + 0170 - 0d 95 23 97 89 8a 97 11-07 4f 98 f5 d7 4a f3 75 ..#......O...J.u + 0180 - a6 39 37 5b 9b c9 53 b3-59 ff 1b ce 22 14 65 79 .97[..S.Y...".ey + 0190 - a3 71 bf e1 ac d5 db 4a-d0 63 88 7b a3 dc 3f 68 .q.....J.c.{..?h + 01a0 - 86 fa 37 07 1a ab 9d 09-5a c8 f7 a6 e3 7f a0 6a ..7.....Z......j + 01b0 - 78 6b 75 48 87 19 2f 87-7b 85 b2 5f 19 72 2a c5 xkuH../.{.._.r*. + 01c0 - be f8 ba 35 e7 be ab 75-e8 60 95 8e 99 01 32 6d ...5...u.`....2m + 01d0 - 31 44 90 6d c8 55 ea b9-ad 76 45 cf 34 9c 9e 6b 1D.m.U...vE.4..k + 01e0 - 47 27 a7 3c 58 6f d7 62-65 5f 2e 3d 59 47 24 99 G'.A.D. + 0050 - fa a2 2d f1 2c 13 2d 21-95 39 09 6e a5 b4 43 fc ..-.,.-!.9.n..C. + 0060 - 7e 18 7b de a5 81 b0 a8-a9 6b 20 2d b6 3e e4 e8 ~.{......k -.>.. + 0070 - 9c 54 35 45 55 97 7a 78-d3 37 79 cb 6f 07 0f b8 .T5EU.zx.7y.o... + 0080 - fb 40 82 7c 1e a5 04 68-de fd 94 a4 a8 23 28 b6 .@.|...h.....#(. + 0090 - 4b ac a7 eb 97 77 a5 6f-b9 88 a7 74 ce eb fe 76 K....w.o...t...v + 00a0 - 17 85 5c 17 16 02 d3 5b-2a 7b 73 8b 9b 3d 65 1d ..\....[*{s..=e. + 00b0 - 9e 68 03 17 7c d0 37 c9-29 90 46 76 48 bf 70 94 .h..|.7.).FvH.p. + 00c0 - f0 3b a5 1c b3 06 5f b5-fd 13 dd 3b b5 d8 62 a0 .;...._....;..b. + 00d0 - f6 f7 13 c5 e0 2a 07 a4-48 c9 d6 27 ba 57 72 41 .....*..H..'.WrA + 00e0 - 76 23 0b 40 fa 56 34 bd-25 07 e4 b7 0d 65 39 6a v#.@.V4.%....e9j + 00f0 - fc bb e1 30 3a 57 84 6b-4a fe 3a 5b 09 7b e2 fe ...0:W.kJ.:[.{.. + 0100 - 28 fc ed da 13 fe d9 cd-aa 2f 02 03 01 00 01 (......../..... +-- BITSTRING at offset 19 -- + 0:d=0 hl=4 l= 266 cons: SEQUENCE + 4:d=1 hl=4 l= 257 prim: INTEGER :A15F236FF920DD14F22E7AEDB2ADBF6FF94898B9DD69CC0C85141146495F47B32CD59DEC10C581C9D31EE879B64A5A2514E1982243E60229B8DE48221C24062D9E3E41F144CFFAA22DF12C132D219539096EA5B443FC7E187BDEA581B0A8A96B202DB63EE4E89C54354555977A78D33779CB6F070FB8FB40827C1EA50468DEFD94A4A82328B64BACA7EB9777A56FB988A774CEEBFE7617855C171602D35B2A7B738B9B3D651D9E6803177CD037C92990467648BF7094F03BA51CB3065FB5FD13DD3BB5D862A0F6F713C5E02A07A448C9D627BA57724176230B40FA5634BD2507E4B70D65396AFCBBE1303A57846B4AFE3A5B097BE2FE28FCEDDA13FED9CDAA2F + 265:d=1 hl=2 l= 3 prim: INTEGER :010001 diff --git a/rust/gel-jwt/src/testcases/rsa2048-pub-pkcs8.pem b/rust/gel-jwt/src/testcases/rsa2048-pub-pkcs8.pem new file mode 100644 index 000000000000..35d92d391830 --- /dev/null +++ b/rust/gel-jwt/src/testcases/rsa2048-pub-pkcs8.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoV8jb/kg3RTyLnrtsq2/ +b/lImLndacwMhRQRRklfR7Ms1Z3sEMWBydMe6Hm2SlolFOGYIkPmAim43kgiHCQG +LZ4+QfFEz/qiLfEsEy0hlTkJbqW0Q/x+GHvepYGwqKlrIC22PuTonFQ1RVWXenjT +N3nLbwcPuPtAgnwepQRo3v2UpKgjKLZLrKfrl3elb7mIp3TO6/52F4VcFxYC01sq +e3OLmz1lHZ5oAxd80DfJKZBGdki/cJTwO6UcswZftf0T3Tu12GKg9vcTxeAqB6RI +ydYnuldyQXYjC0D6VjS9JQfktw1lOWr8u+EwOleEa0r+OlsJe+L+KPzt2hP+2c2q +LwIDAQAB +-----END PUBLIC KEY-----