From f933ace0073f276af9962c9a517deee42beb4914 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 4 Jan 2022 22:28:00 +0100 Subject: [PATCH] New JWT/JOSE crate Still WIP, needs to handle time related claims --- Cargo.lock | 530 ++++++++++++------------ crates/cli/src/server.rs | 11 +- crates/config/Cargo.toml | 11 +- crates/config/src/lib.rs | 2 +- crates/config/src/oauth2.rs | 360 ++++------------ crates/handlers/Cargo.toml | 12 +- crates/handlers/src/lib.rs | 6 +- crates/handlers/src/oauth2/keys.rs | 20 +- crates/handlers/src/oauth2/mod.rs | 8 +- crates/handlers/src/oauth2/token.rs | 41 +- crates/jose/Cargo.toml | 31 ++ crates/jose/src/iana.rs | 295 +++++++++++++ crates/jose/src/jwk.rs | 364 ++++++++++++++++ crates/jose/src/jwt.rs | 267 ++++++++++++ crates/jose/src/keystore.rs | 510 +++++++++++++++++++++++ crates/jose/src/lib.rs | 35 ++ crates/storage/src/user.rs | 2 + crates/warp-utils/Cargo.toml | 2 +- crates/warp-utils/src/filters/client.rs | 151 ++----- crates/warp-utils/src/filters/mod.rs | 10 - 20 files changed, 1942 insertions(+), 726 deletions(-) create mode 100644 crates/jose/Cargo.toml create mode 100644 crates/jose/src/iana.rs create mode 100644 crates/jose/src/jwk.rs create mode 100644 crates/jose/src/jwt.rs create mode 100644 crates/jose/src/keystore.rs create mode 100644 crates/jose/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 0850f1d7..c8803513 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", "rand_core", ] @@ -35,7 +35,7 @@ checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ "getrandom", "once_cell", - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -73,9 +73,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" dependencies = [ "backtrace", ] @@ -235,25 +235,13 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "0.19.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blake2" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a58bdf5134c5beae6fc382002c4d88950bad1feea20f8f7165494b6b43b049de" dependencies = [ - "digest 0.10.0", + "digest 0.10.1", ] [[package]] @@ -274,7 +262,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] @@ -283,7 +271,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] @@ -457,14 +445,14 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] name = "clap" -version = "3.0.0-rc.4" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "967965e82fc46fee1a88147a7a977a66d615ed5f83eb95b18577b342c08f90ff" +checksum = "d01c9347757e131122b19cd19a05c85805b68c2352a97b623efdc3c295290299" dependencies = [ "atty", "bitflags", @@ -479,11 +467,11 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.0.0-rc.4" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85946d4034625800196413478a1c6d3a57c12785e1f3970e590e0137dfa07342" +checksum = "448c8b00367288ad41804ac7a9e0fe58f2324a36901cb5d6b6db58be86d1db8f" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-error", "proc-macro2", "quote", @@ -492,15 +480,15 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "const_fn" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" [[package]] name = "cookie" @@ -509,7 +497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" dependencies = [ "time 0.2.27", - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -612,11 +600,11 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.2.11" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", "rand_core", "subtle", "zeroize", @@ -624,11 +612,11 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567569e659735adb39ff2d4c20600f7cd78be5471f8c58ab162bce3c03fdbc5f" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] @@ -637,7 +625,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", "subtle", ] @@ -694,12 +682,13 @@ checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" [[package]] name = "der" -version = "0.4.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" dependencies = [ "const-oid", "crypto-bigint", + "pem-rfc7468", ] [[package]] @@ -723,26 +712,26 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", ] [[package]] name = "digest" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8549e6bfdecd113b7e221fe60b433087f6957387a20f8118ebca9b12af19143d" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" dependencies = [ "block-buffer 0.10.0", "crypto-common", - "generic-array 0.14.4", + "generic-array 0.14.5", "subtle", ] [[package]] name = "dirs" -version = "3.0.2" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ "dirs-sys", ] @@ -778,13 +767,13 @@ checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" [[package]] name = "ecdsa" -version = "0.12.4" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" +checksum = "e91ae02c7618ee05108cd86a0be2f5586d1f0d965bede7ecfd46815f1b860227" dependencies = [ "der", "elliptic-curve", - "hmac", + "rfc6979", "signature", ] @@ -799,16 +788,18 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.10.6" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b" +checksum = "decb3a27ea454a5f23f96eb182af0671c12694d64ecc33dada74edd1301f6cfc" dependencies = [ "crypto-bigint", + "der", "ff", - "generic-array 0.14.4", + "generic-array 0.14.5", "group", - "pkcs8", + "pem-rfc7468", "rand_core", + "sec1", "subtle", "zeroize", ] @@ -830,9 +821,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "ff" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" +checksum = "b2958d04124b9f27f175eaeb9a9f383d026098aa837eadd8ba22c11f13a05b9e" dependencies = [ "rand_core", "subtle", @@ -851,7 +842,7 @@ dependencies = [ "serde_yaml", "tempfile", "uncased", - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -888,12 +879,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "futures" version = "0.1.31" @@ -902,9 +887,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "futures" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" +checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" dependencies = [ "futures-channel", "futures-core", @@ -917,9 +902,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" +checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" dependencies = [ "futures-core", "futures-sink", @@ -927,15 +912,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" [[package]] name = "futures-executor" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" dependencies = [ "futures-core", "futures-task", @@ -955,15 +940,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" [[package]] name = "futures-macro" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" +checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" dependencies = [ "proc-macro2", "quote", @@ -972,21 +957,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" +checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" [[package]] name = "futures-task" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" [[package]] name = "futures-util" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" dependencies = [ "futures 0.1.31", "futures-channel", @@ -1012,12 +997,12 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -1063,9 +1048,9 @@ dependencies = [ [[package]] name = "group" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" +checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ "ff", "rand_core", @@ -1091,12 +1076,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "hashbrown" version = "0.11.2" @@ -1163,6 +1142,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1190,13 +1175,13 @@ dependencies = [ [[package]] name = "http" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes 1.1.0", "fnv", - "itoa", + "itoa 1.0.1", ] [[package]] @@ -1249,7 +1234,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 0.4.8", "pin-project-lite", "socket2", "tokio", @@ -1268,7 +1253,7 @@ dependencies = [ "hyper", "rustls 0.20.2", "tokio", - "tokio-rustls 0.23.1", + "tokio-rustls 0.23.2", ] [[package]] @@ -1339,9 +1324,9 @@ dependencies = [ [[package]] name = "inlinable_string" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3094308123a0e9fd59659ce45e22de9f53fc1d2ac6e1feb9fef988e4f76cad77" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" [[package]] name = "instant" @@ -1375,11 +1360,11 @@ checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] name = "iri-string" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326cf970601ca3f0a79e1727b606009d75a8104a0489d146982828141b2c044" +checksum = "8f0f7638c1e223529f1bfdc48c8b133b9e0b434094d1d28473161ee48b235f78" dependencies = [ - "nom 6.1.2", + "nom 7.1.0", ] [[package]] @@ -1397,6 +1382,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "js-sys" version = "0.3.55" @@ -1406,40 +1397,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jwt-compact" -version = "0.5.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8744ba39fb09cfe9ce0666650e0e338ed1a031809b10362a544ef05588c2380b" -dependencies = [ - "anyhow", - "base64ct", - "chrono", - "hmac", - "k256", - "rand_core", - "rsa", - "serde", - "serde_cbor", - "serde_json", - "sha2 0.9.8", - "smallvec", - "subtle", - "zeroize", -] - -[[package]] -name = "k256" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea" -dependencies = [ - "cfg-if 1.0.0", - "ecdsa", - "elliptic-curve", - "sha2 0.9.8", -] - [[package]] name = "language-tags" version = "0.3.2" @@ -1460,9 +1417,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.111" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e167738f1866a7ec625567bae89ca0d44477232a4f7c52b1c7f2adc2c98804f" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" [[package]] name = "libm" @@ -1508,7 +1465,7 @@ dependencies = [ "argon2", "clap", "dotenv", - "futures 0.3.18", + "futures 0.3.19", "hyper", "indoc", "mas-config", @@ -1547,8 +1504,8 @@ dependencies = [ "elliptic-curve", "figment", "indoc", - "jwt-compact", - "k256", + "mas-jose", + "p256", "pkcs8", "rand", "rsa", @@ -1582,25 +1539,31 @@ version = "0.1.0" dependencies = [ "anyhow", "argon2", + "chacha20poly1305", "chrono", + "crc", "data-encoding", + "elliptic-curve", "headers", "hyper", - "jwt-compact", + "indoc", "mas-config", "mas-data-model", + "mas-jose", "mas-static-files", "mas-storage", "mas-templates", "mas-warp-utils", "mime", "oauth2-types", + "pkcs8", "rand", + "rsa", "serde", "serde_json", "serde_urlencoded", "serde_with", - "sha2 0.10.0", + "sha2 0.9.8", "sqlx", "thiserror", "tokio", @@ -1609,6 +1572,35 @@ dependencies = [ "warp", ] +[[package]] +name = "mas-jose" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64ct", + "crypto-mac", + "digest 0.9.0", + "ecdsa", + "elliptic-curve", + "hmac", + "p256", + "pkcs1", + "pkcs8", + "rand", + "rsa", + "sec1", + "serde", + "serde_json", + "serde_with", + "sha2 0.9.8", + "signature", + "thiserror", + "tokio", + "url", + "zeroize", +] + [[package]] name = "mas-static-files" version = "0.1.0" @@ -1684,9 +1676,9 @@ dependencies = [ "data-encoding", "headers", "hyper", - "jwt-compact", "mas-config", "mas-data-model", + "mas-jose", "mas-storage", "mas-templates", "mime", @@ -1825,18 +1817,6 @@ dependencies = [ "version_check 0.1.5", ] -[[package]] -name = "nom" -version = "6.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" -dependencies = [ - "bitvec", - "funty", - "memchr", - "version_check 0.9.3", -] - [[package]] name = "nom" version = "7.1.0" @@ -1845,7 +1825,7 @@ checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" dependencies = [ "memchr", "minimal-lexical", - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -1908,9 +1888,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -1946,9 +1926,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "opaque-debug" @@ -1978,7 +1958,7 @@ dependencies = [ "crossbeam-channel 0.5.1", "dashmap", "fnv", - "futures 0.3.18", + "futures 0.3.19", "js-sys", "lazy_static", "percent-encoding", @@ -2029,7 +2009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f19d4b43842433c420c548c985d158f5628bba5b518e0be64627926d19889992" dependencies = [ "async-trait", - "futures 0.3.18", + "futures 0.3.19", "http", "opentelemetry", "prost", @@ -2085,6 +2065,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "p256" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e0c5310031b5d4528ac6534bccc1446c289ac45c47b277d5aa91089c5f74fa" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sec1", + "sha2 0.9.8", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -2181,9 +2173,9 @@ dependencies = [ [[package]] name = "pem-rfc7468" -version = "0.2.4" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e93a3b1cc0510b03020f33f21e62acdde3dcaef432edc95bea377fbd4c2cd4" +checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" dependencies = [ "base64ct", ] @@ -2288,18 +2280,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", @@ -2308,9 +2300,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -2320,24 +2312,22 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" -version = "0.2.4" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "116bee8279d783c0cf370efa1a94632f2108e5ef0bb32df31f051647810a4e2c" +checksum = "a271c8f8b7dd4268a1df4f269e2f4b317c906ac00c998ca0f3394986e703dd86" dependencies = [ "der", - "pem-rfc7468", + "pkcs8", "zeroize", ] [[package]] name = "pkcs8" -version = "0.7.6" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" dependencies = [ "der", - "pem-rfc7468", - "pkcs1", "spki", "zeroize", ] @@ -2355,9 +2345,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro-error" @@ -2369,7 +2359,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -2380,7 +2370,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -2391,9 +2381,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] @@ -2407,7 +2397,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "version_check 0.9.3", + "version_check 0.9.4", "yansi", ] @@ -2428,7 +2418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603" dependencies = [ "bytes 1.1.0", - "heck", + "heck 0.3.3", "itertools", "log", "multimap", @@ -2470,19 +2460,13 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" - [[package]] name = "rand" version = "0.8.4" @@ -2579,9 +2563,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5" +checksum = "7c4e0a76dc12a116108933f6301b95e83634e0c47b0afbed6abbaa0601e99258" dependencies = [ "base64 0.13.0", "bytes 1.1.0", @@ -2606,15 +2590,26 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls 0.23.1", + "tokio-rustls 0.23.2", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.22.2", "winreg", ] +[[package]] +name = "rfc6979" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + [[package]] name = "ring" version = "0.16.20" @@ -2633,12 +2628,10 @@ dependencies = [ [[package]] name = "rsa" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" +source = "git+https://github.com/sandhose/rsa.git?branch=bump-pkcs#44f4b93bebef6258c3f1590172d4d507bb92808b" dependencies = [ "byteorder", "digest 0.9.0", - "lazy_static", "num-bigint-dig", "num-integer", "num-iter", @@ -2840,6 +2833,19 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sec1" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" +dependencies = [ + "der", + "generic-array 0.14.5", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.4.2" @@ -2880,9 +2886,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.131" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" dependencies = [ "serde_derive", ] @@ -2900,21 +2906,11 @@ dependencies = [ "thiserror", ] -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half", - "serde", -] - [[package]] name = "serde_derive" -version = "1.0.131" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", "quote", @@ -2934,12 +2930,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.72" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" dependencies = [ "indexmap", - "itoa", + "itoa 1.0.1", "ryu", "serde", ] @@ -2951,7 +2947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ "form_urlencoded", - "itoa", + "itoa 0.4.8", "ryu", "serde", ] @@ -2962,6 +2958,7 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad6056b4cb69b6e43e3a0f055def223380baecc99da683884f205bf347f7c4b3" dependencies = [ + "base64 0.13.0", "chrono", "hex", "rustversion", @@ -3045,7 +3042,7 @@ checksum = "900d964dd36bb15bcf2f2b35694c072feab74969a54f2bbeec7a2d725d2bdcb6" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.0", + "digest 0.10.1", ] [[package]] @@ -3068,9 +3065,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" +checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" dependencies = [ "digest 0.9.0", "rand_core", @@ -3121,10 +3118,11 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spki" -version = "0.4.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" +checksum = "964d3a6f8b7ef6d6d20887f4c30c4848f4ffa05f600c87277d30a5b4fe32cb4b" dependencies = [ + "base64ct", "der", ] @@ -3141,9 +3139,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7911b0031a0247af40095838002999c7a52fba29d9739e93326e71a5a1bc9d43" +checksum = "692749de69603d81e016212199d73a2e14ee20e2def7d7914919e8db5d4d48b9" dependencies = [ "sqlx-core", "sqlx-macros", @@ -3151,9 +3149,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec89bfaca8f7737439bad16d52b07f1ccd0730520d3bf6ae9d069fe4b641fb1" +checksum = "518be6f6fff5ca76f985d434f9c37f3662af279642acf730388f271dff7b9016" dependencies = [ "ahash", "atoi", @@ -3176,7 +3174,7 @@ dependencies = [ "hex", "hmac", "indexmap", - "itoa", + "itoa 1.0.1", "libc", "log", "md-5", @@ -3198,19 +3196,19 @@ dependencies = [ "tokio-stream", "url", "webpki 0.21.4", - "webpki-roots", + "webpki-roots 0.21.1", "whoami", ] [[package]] name = "sqlx-macros" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584866c833511b1a152e87a7ee20dee2739746f60c858b3c5209150bc4b466f5" +checksum = "38e45140529cf1f90a5e1c2e561500ca345821a1c513652c8f486bbf07407cc8" dependencies = [ "dotenv", "either", - "heck", + "heck 0.3.3", "hex", "once_cell", "proc-macro2", @@ -3226,9 +3224,9 @@ dependencies = [ [[package]] name = "sqlx-rt" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d1bd069de53442e7a320f525a6d4deb8bb0621ac7a55f7eccbc2b58b57f43d0" +checksum = "8061cbaa91ee75041514f67a09398c65a64efed72c90151ecd47593bad53da99" dependencies = [ "once_cell", "tokio", @@ -3241,7 +3239,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" dependencies = [ - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -3340,9 +3338,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.82" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" dependencies = [ "proc-macro2", "quote", @@ -3361,12 +3359,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tempfile" version = "3.2.0" @@ -3491,7 +3483,7 @@ dependencies = [ "standback", "stdweb", "time-macros", - "version_check 0.9.3", + "version_check 0.9.4", "winapi", ] @@ -3535,11 +3527,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg 1.0.1", "bytes 1.1.0", "libc", "memchr", @@ -3555,9 +3546,9 @@ dependencies = [ [[package]] name = "tokio-io-timeout" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90c49f106be240de154571dd31fbe48acb10ba6c6dd6f6517ad603abffa42de9" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite", "tokio", @@ -3565,9 +3556,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -3587,9 +3578,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4baa378e417d780beff82bf54ceb0d195193ea6a00c14e22359e7f39456b5689" +checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" dependencies = [ "rustls 0.20.2", "tokio", @@ -3810,9 +3801,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245da694cc7fc4729f3f418b304cb57789f1bed2a78c575407ab8a23f53cb4d3" +checksum = "5d81bfa81424cc98cb034b837c985b7a290f592e5b4322f353f94a0ab0f9f594" dependencies = [ "ansi_term", "lazy_static", @@ -3873,9 +3864,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" @@ -3889,7 +3880,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" dependencies = [ - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -3948,7 +3939,7 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ - "version_check 0.9.3", + "version_check 0.9.4", ] [[package]] @@ -3996,7 +3987,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ - "generic-array 0.14.4", + "generic-array 0.14.5", "subtle", ] @@ -4033,9 +4024,9 @@ checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" @@ -4168,7 +4159,7 @@ checksum = "1afbab1186833c9b34f64132b80ed4b373ed4eab6f9efa1f55430835200f0a28" dependencies = [ "anyhow", "bytes 1.1.0", - "futures 0.3.18", + "futures 0.3.19", "maplit", "serde", "serde_bser", @@ -4217,6 +4208,15 @@ dependencies = [ "webpki 0.21.4", ] +[[package]] +name = "webpki-roots" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +dependencies = [ + "webpki 0.22.0", +] + [[package]] name = "which" version = "4.2.2" @@ -4278,12 +4278,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 6c3901bf..bcc65a96 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -14,6 +14,7 @@ use std::{ net::{SocketAddr, TcpListener}, + sync::Arc, time::Duration, }; @@ -236,6 +237,14 @@ impl ServerCommand { queue.recuring(Duration::from_secs(15), mas_tasks::cleanup_expired(&pool)); queue.start(); + // Initialize the key store + let key_store = config + .oauth2 + .key_store() + .context("could not import keys from config")?; + // Wrap the key store in an Arc + let key_store = Arc::new(key_store); + // Load and compile the templates let templates = Templates::load_from_config(&config.templates) .await @@ -254,7 +263,7 @@ impl ServerCommand { } // Start the server - let root = mas_handlers::root(&pool, &templates, &config); + let root = mas_handlers::root(&pool, &templates, &key_store, &config); let warp_service = warp::service(root); diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index d72df355..6a74d409 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -24,10 +24,11 @@ serde_json = "1.0.72" sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] } rand = "0.8.4" -rsa = "0.5.0" -k256 = "0.9.6" -pkcs8 = { version = "0.7.6", features = ["pem"] } -elliptic-curve = { version = "0.10.6", features = ["pem"] } -jwt-compact = { version = "0.5.0-beta.1", features = ["with_rsa", "k256"] } +rsa = { git = "https://github.com/sandhose/rsa.git", branch = "bump-pkcs" } +p256 = { version = "0.10.0", features = ["ecdsa", "pem", "pkcs8"] } +pkcs8 = { version = "0.8.0", features = ["pem"] } +elliptic-curve = { version = "0.11.6", features = ["pem", "pkcs8"] } indoc = "1.0.3" + +mas-jose = { path = "../jose" } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 62c86580..443552db 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -38,7 +38,7 @@ pub use self::{ csrf::CsrfConfig, database::DatabaseConfig, http::HttpConfig, - oauth2::{Algorithm, KeySet, OAuth2ClientConfig, OAuth2Config}, + oauth2::{OAuth2ClientConfig, OAuth2Config}, telemetry::{ MetricsConfig, MetricsExporterConfig, Propagator, TelemetryConfig, TracingConfig, TracingExporterConfig, diff --git a/crates/config/src/oauth2.rs b/crates/config/src/oauth2.rs index cce58067..fe4c9c47 100644 --- a/crates/config/src/oauth2.rs +++ b/crates/config/src/oauth2.rs @@ -14,19 +14,14 @@ use anyhow::Context; use async_trait::async_trait; -use jwt_compact::{ - alg::{self, StrongAlg, StrongKey}, - jwk::JsonWebKey, - AlgorithmExt, Claims, Header, +use mas_jose::StaticKeystore; +use pkcs8::{DecodePrivateKey, EncodePrivateKey}; +use rsa::{ + pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey}, + RsaPrivateKey, }; -use pkcs8::{FromPrivateKey, ToPrivateKey}; -use rsa::RsaPrivateKey; use schemars::JsonSchema; -use serde::{ - de::{MapAccess, Visitor}, - ser::SerializeStruct, - Deserialize, Serialize, -}; +use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use thiserror::Error; use tokio::task; @@ -35,257 +30,17 @@ use url::Url; use super::ConfigurationSection; -// TODO: a lot of the signing logic should go out somewhere else - -const RS256: StrongAlg = StrongAlg(alg::Rsa::rs256()); - -#[derive(Serialize, Deserialize, Clone, Copy, Debug)] -#[serde(rename_all = "UPPERCASE")] -pub enum Algorithm { - Rs256, - Es256k, +#[derive(JsonSchema, Serialize, Deserialize, Clone, Copy, Debug)] +#[serde(rename_all = "lowercase")] +pub enum KeyType { + Rsa, + Ecdsa, } -#[derive(Serialize, Clone)] -pub struct Jwk { - kid: String, - alg: Algorithm, - - #[serde(flatten)] - inner: serde_json::Value, -} - -#[derive(Serialize, Clone)] -pub struct Jwks { - keys: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(transparent)] -pub struct KeySet(Vec); - -impl KeySet { - #[must_use] - pub fn to_public_jwks(&self) -> Jwks { - let keys = self.0.iter().map(Key::to_public_jwk).collect(); - Jwks { keys } - } - - #[tracing::instrument(err)] - pub async fn token( - &self, - alg: Algorithm, - header: Header, - claims: Claims, - ) -> anyhow::Result - where - T: std::fmt::Debug + Serialize + Send + Sync + 'static, - { - match alg { - Algorithm::Rs256 => { - let (kid, key) = self - .0 - .iter() - .find_map(Key::rsa) - .context("could not find RSA key")?; - let header = header.with_key_id(kid); - - // TODO: store them as strong keys - let key = StrongKey::try_from(key.clone())?; - task::spawn_blocking(move || { - RS256 - .token(header, &claims, &key) - .context("failed to sign token") - }) - .await? - } - Algorithm::Es256k => { - // TODO: make this const with lazy_static? - let es256k: alg::Es256k = alg::Es256k::default(); - let (kid, key) = self - .0 - .iter() - .find_map(Key::ecdsa) - .context("could not find ECDSA key")?; - let key = k256::ecdsa::SigningKey::from(key); - let header = header.with_key_id(kid); - // TODO: use StrongAlg - - task::spawn_blocking(move || { - es256k - .token(header, &claims, &key) - .context("failed to sign token") - }) - .await? - } - } - } -} - -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum Key { - Rsa { - key: Box, - kid: String, - }, - Ecdsa { - key: k256::SecretKey, - kid: String, - }, -} - -impl Key { - fn from_ecdsa(key: k256::SecretKey) -> Self { - // TODO: hash the key and use as KID - let kid = String::from("ecdsa-kid"); - Self::Ecdsa { kid, key } - } - - fn from_ecdsa_pem(key: &str) -> anyhow::Result { - let key = k256::SecretKey::from_pkcs8_pem(key)?; - Ok(Self::from_ecdsa(key)) - } - - fn from_rsa(key: RsaPrivateKey) -> Self { - // TODO: hash the key and use as KID - let kid = String::from("rsa-kid"); - Self::Rsa { - kid, - key: Box::new(key), - } - } - - fn from_rsa_pem(key: &str) -> anyhow::Result { - let key = RsaPrivateKey::from_pkcs8_pem(key)?; - Ok(Self::from_rsa(key)) - } - - fn to_public_jwk(&self) -> Jwk { - match self { - Key::Rsa { key, kid } => { - let pubkey = key.to_public_key(); - let inner = JsonWebKey::from(&pubkey); - let inner = serde_json::to_value(&inner).unwrap(); - let kid = kid.to_string(); - let alg = Algorithm::Rs256; - Jwk { kid, alg, inner } - } - Key::Ecdsa { key, kid } => { - let pubkey = k256::ecdsa::VerifyingKey::from(key.public_key()); - let inner = JsonWebKey::from(&pubkey); - let inner = serde_json::to_value(&inner).unwrap(); - let kid = kid.to_string(); - let alg = Algorithm::Es256k; - Jwk { kid, alg, inner } - } - } - } - - fn rsa(&self) -> Option<(&str, &RsaPrivateKey)> { - match self { - Key::Rsa { key, kid } => Some((kid, key)), - _ => None, - } - } - - fn ecdsa(&self) -> Option<(&str, &k256::SecretKey)> { - match self { - Key::Ecdsa { key, kid } => Some((kid, key)), - _ => None, - } - } -} - -impl Serialize for Key { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut map = serializer.serialize_struct("Key", 2)?; - match self { - Key::Rsa { key, kid: _ } => { - map.serialize_field("type", "rsa")?; - let pem = key.to_pkcs8_pem().map_err(serde::ser::Error::custom)?; - map.serialize_field("key", pem.as_str())?; - } - Key::Ecdsa { key, kid: _ } => { - map.serialize_field("type", "ecdsa")?; - let pem = key.to_pkcs8_pem().map_err(serde::ser::Error::custom)?; - map.serialize_field("key", pem.as_str())?; - } - } - - map.end() - } -} - -impl<'de> Deserialize<'de> for Key { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize, Debug)] - #[serde(field_identifier, rename_all = "lowercase")] - enum Field { - Type, - Key, - } - - #[derive(Deserialize)] - #[serde(rename_all = "lowercase")] - enum KeyType { - Rsa, - Ecdsa, - } - - struct KeyVisitor; - - impl<'de> Visitor<'de> for KeyVisitor { - type Value = Key; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("struct Key") - } - - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - let mut key_type = None; - let mut key_key = None; - while let Some(key) = map.next_key()? { - match key { - Field::Type => { - if key_type.is_some() { - return Err(serde::de::Error::duplicate_field("type")); - } - key_type = Some(map.next_value()?); - } - Field::Key => { - if key_key.is_some() { - return Err(serde::de::Error::duplicate_field("key")); - } - key_key = Some(map.next_value()?); - } - } - } - let key_type: KeyType = - key_type.ok_or_else(|| serde::de::Error::missing_field("type"))?; - let key_key: String = - key_key.ok_or_else(|| serde::de::Error::missing_field("key"))?; - - match key_type { - KeyType::Rsa => Key::from_rsa_pem(&key_key).map_err(serde::de::Error::custom), - KeyType::Ecdsa => { - Key::from_ecdsa_pem(&key_key).map_err(serde::de::Error::custom) - } - } - } - } - - deserializer.deserialize_struct("Key", &["type", "key"], KeyVisitor) - } +#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] +pub struct KeyConfig { + r#type: KeyType, + key: String, } #[skip_serializing_none] @@ -339,8 +94,8 @@ pub struct OAuth2Config { #[serde(default)] pub clients: Vec, - #[schemars(with = "Vec")] // TODO: this is a lie - pub keys: KeySet, + #[serde(default)] + pub keys: Vec, } impl OAuth2Config { @@ -350,6 +105,25 @@ impl OAuth2Config { .join(".well-known/openid-configuration") .expect("could not build discovery url") } + + pub fn key_store(&self) -> anyhow::Result { + let mut store = StaticKeystore::new(); + + for key in &self.keys { + match key.r#type { + KeyType::Ecdsa => { + let key = p256::SecretKey::from_pkcs8_pem(&key.key)?; + store.add_ecdsa_key(key.into())?; + } + KeyType::Rsa => { + let key = rsa::RsaPrivateKey::from_pkcs1_pem(&key.key)?; + store.add_rsa_key(key)?; + } + } + } + + Ok(store) + } } #[async_trait] @@ -373,52 +147,66 @@ impl ConfigurationSection<'_> for OAuth2Config { }) .await .context("could not join blocking task")??; + let rsa_key = KeyConfig { + r#type: KeyType::Rsa, + key: rsa_key.to_pkcs1_pem(pkcs8::LineEnding::LF)?.to_string(), + }; let span = tracing::info_span!("ecdsa"); let ecdsa_key = task::spawn_blocking(move || { let _entered = span.enter(); let rng = rand::thread_rng(); - let ret = k256::SecretKey::random(rng); + let ret = p256::SecretKey::random(rng); info!("Done generating ECDSA key"); ret }) .await .context("could not join blocking task")?; + let ecdsa_key = KeyConfig { + r#type: KeyType::Ecdsa, + key: ecdsa_key.to_pkcs8_pem(pkcs8::LineEnding::LF)?.to_string(), + }; Ok(Self { issuer: default_oauth2_issuer(), clients: Vec::new(), - keys: KeySet(vec![Key::from_rsa(rsa_key), Key::from_ecdsa(ecdsa_key)]), + keys: vec![rsa_key, ecdsa_key], }) } fn test() -> Self { - let rsa_key = Key::from_rsa_pem(indoc::indoc! {r#" - -----BEGIN PRIVATE KEY----- - MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN - QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU - scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh - 3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE - vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw - N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622 - tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl - Gh7BNzCeN+D6 - -----END PRIVATE KEY----- - "#}) - .unwrap(); - let ecdsa_key = Key::from_ecdsa_pem(indoc::indoc! {r#" - -----BEGIN PRIVATE KEY----- - MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA - NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn - OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC - -----END PRIVATE KEY----- - "#}) - .unwrap(); + let rsa_key = KeyConfig { + r#type: KeyType::Rsa, + key: indoc::indoc! {r#" + -----BEGIN PRIVATE KEY----- + MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN + QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU + scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh + 3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE + vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw + N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622 + tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl + Gh7BNzCeN+D6 + -----END PRIVATE KEY----- + "#} + .to_string(), + }; + let ecdsa_key = KeyConfig { + r#type: KeyType::Ecdsa, + key: indoc::indoc! {r#" + -----BEGIN PRIVATE KEY----- + MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA + NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn + OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC + -----END PRIVATE KEY----- + "#} + .to_string(), + }; Self { issuer: default_oauth2_issuer(), clients: Vec::new(), - keys: KeySet(vec![rsa_key, ecdsa_key]), + keys: vec![rsa_key, ecdsa_key], } } } diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 3c48d682..d25fc25d 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -36,8 +36,12 @@ serde_urlencoded = "0.7.0" argon2 = { version = "0.3.2", features = ["password-hash"] } # Crypto, hashing and signing stuff -sha2 = "0.10.0" -jwt-compact = { version = "0.5.0-beta.1", features = ["with_rsa", "k256"] } +rsa = { git = "https://github.com/sandhose/rsa.git", branch = "bump-pkcs" } +pkcs8 = { version = "0.8.0", features = ["pem"] } +elliptic-curve = { version = "0.11.6", features = ["pem"] } +chacha20poly1305 = { version = "0.9.0", features = ["std"] } +sha2 = "0.9.8" +crc = "2.1.0" # Various data types and utilities data-encoding = "2.3.2" @@ -54,3 +58,7 @@ mas-templates = { path = "../templates" } mas-static-files = { path = "../static-files" } mas-storage = { path = "../storage" } mas-warp-utils = { path = "../warp-utils" } +mas-jose = { path = "../jose" } + +[dev-dependencies] +indoc = "1.0.3" diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 0c704d1e..4cfc244e 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -22,7 +22,10 @@ #![allow(clippy::implicit_hasher)] #![allow(clippy::unused_async)] // Some warp filters need that +use std::sync::Arc; + use mas_config::RootConfig; +use mas_jose::StaticKeystore; use mas_static_files::filter as static_files; use mas_templates::Templates; use sqlx::PgPool; @@ -38,10 +41,11 @@ use self::{health::filter as health, oauth2::filter as oauth2, views::filter as pub fn root( pool: &PgPool, templates: &Templates, + key_store: &Arc, config: &RootConfig, ) -> BoxedFilter<(impl Reply,)> { let health = health(pool); - let oauth2 = oauth2(pool, templates, &config.oauth2, &config.cookies); + let oauth2 = oauth2(pool, templates, key_store, &config.oauth2, &config.cookies); let views = views( pool, templates, diff --git a/crates/handlers/src/oauth2/keys.rs b/crates/handlers/src/oauth2/keys.rs index 8cec8607..d33005ce 100644 --- a/crates/handlers/src/oauth2/keys.rs +++ b/crates/handlers/src/oauth2/keys.rs @@ -12,16 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_config::OAuth2Config; -use warp::{filters::BoxedFilter, Filter, Reply}; +use std::sync::Arc; -pub(super) fn filter(config: &OAuth2Config) -> BoxedFilter<(Box,)> { - let jwks = config.keys.to_public_jwks(); +use mas_jose::{ExportJwks, StaticKeystore}; +use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; +pub(super) fn filter(key_store: &Arc) -> BoxedFilter<(Box,)> { + let key_store = key_store.clone(); warp::path!("oauth2" / "keys.json") - .and(warp::get().map(move || { - let ret: Box = Box::new(warp::reply::json(&jwks)); - ret - })) + .and(warp::get().map(move || key_store.clone()).and_then(get)) .boxed() } + +async fn get(key_store: Arc) -> Result, Rejection> { + let jwks = key_store.export_jwks().await; + + Ok(Box::new(warp::reply::json(&jwks))) +} diff --git a/crates/handlers/src/oauth2/mod.rs b/crates/handlers/src/oauth2/mod.rs index eaa60998..01572b75 100644 --- a/crates/handlers/src/oauth2/mod.rs +++ b/crates/handlers/src/oauth2/mod.rs @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; + use hyper::Method; use mas_config::{CookiesConfig, OAuth2Config}; +use mas_jose::StaticKeystore; use mas_templates::Templates; use mas_warp_utils::filters::cors::cors; use sqlx::PgPool; @@ -36,15 +39,16 @@ use self::{ pub fn filter( pool: &PgPool, templates: &Templates, + key_store: &Arc, oauth2_config: &OAuth2Config, cookies_config: &CookiesConfig, ) -> BoxedFilter<(impl Reply,)> { let discovery = discovery(oauth2_config); - let keys = keys(oauth2_config); + let keys = keys(key_store); let authorization = authorization(pool, templates, oauth2_config, cookies_config); let userinfo = userinfo(pool, oauth2_config); let introspection = introspection(pool, oauth2_config); - let token = token(pool, oauth2_config); + let token = token(pool, key_store, oauth2_config); let filter = discovery .or(keys) diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 538f8cc1..712c9cbf 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -12,14 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; + use anyhow::Context; use chrono::{DateTime, Duration, Utc}; use data_encoding::BASE64URL_NOPAD; use headers::{CacheControl, Pragma}; use hyper::StatusCode; -use jwt_compact::{Claims, Header, TimeOptions}; -use mas_config::{KeySet, OAuth2ClientConfig, OAuth2Config}; +use mas_config::{OAuth2ClientConfig, OAuth2Config}; use mas_data_model::{AuthorizationGrantStage, TokenType}; +use mas_jose::{DecodedJsonWebToken, JsonWebSignatureAlgorithm, SigningKeystore, StaticKeystore}; use mas_storage::{ oauth2::{ access_token::{add_access_token, revoke_access_token}, @@ -30,7 +32,7 @@ use mas_storage::{ }; use mas_warp_utils::{ errors::WrapError, - filters::{client::client_authentication, database::connection, with_keys}, + filters::{client::client_authentication, database::connection}, reply::with_typed_header, }; use oauth2_types::{ @@ -89,7 +91,12 @@ where Err(Error { json, status }.into()) } -pub fn filter(pool: &PgPool, oauth2_config: &OAuth2Config) -> BoxedFilter<(Box,)> { +pub fn filter( + pool: &PgPool, + key_store: &Arc, + oauth2_config: &OAuth2Config, +) -> BoxedFilter<(Box,)> { + let key_store = key_store.clone(); let audience = oauth2_config .issuer .join("/oauth2/token") @@ -101,7 +108,7 @@ pub fn filter(pool: &PgPool, oauth2_config: &OAuth2Config) -> BoxedFilter<(Box, issuer: Url, mut conn: PoolConnection, ) -> Result, Rejection> { let reply = match req { AccessTokenRequest::AuthorizationCode(grant) => { - let reply = authorization_code_grant(&grant, &client, &keys, issuer, &mut conn).await?; + let reply = + authorization_code_grant(&grant, &client, &key_store, issuer, &mut conn).await?; json(&reply) } AccessTokenRequest::RefreshToken(grant) => { @@ -160,7 +168,7 @@ fn hash(mut hasher: H, token: &str) -> anyhow::Result { async fn authorization_code_grant( grant: &AuthorizationCodeGrant, client: &OAuth2ClientConfig, - keys: &KeySet, + key_store: &StaticKeystore, issuer: Url, conn: &mut PoolConnection, ) -> Result { @@ -245,9 +253,8 @@ async fn authorization_code_grant( .wrap_error()?; let id_token = if session.scope.contains(&OPENID) { - let header = Header::default(); - let options = TimeOptions::default(); - let claims = Claims::new(CustomClaims { + // TODO: time-related claims + let claims = CustomClaims { issuer, subject: browser_session.user.sub.clone(), audiences: vec![client.client_id.clone()], @@ -258,15 +265,15 @@ async fn authorization_code_grant( .map(|a| a.created_at), at_hash: hash(Sha256::new(), &access_token_str).wrap_error()?, c_hash: hash(Sha256::new(), &grant.code).wrap_error()?, - }) - .set_duration_and_issuance(&options, Duration::minutes(30)); - let id_token = keys - .token(mas_config::Algorithm::Rs256, header, claims) + }; + let header = key_store + .prepare_header(JsonWebSignatureAlgorithm::Rs256) .await - .context("could not sign ID token") .wrap_error()?; + let id_token = DecodedJsonWebToken::new(header, claims); + let id_token = id_token.sign(key_store).await.wrap_error()?; - Some(id_token) + Some(id_token.serialize()) } else { None }; diff --git a/crates/jose/Cargo.toml b/crates/jose/Cargo.toml new file mode 100644 index 00000000..4be4d999 --- /dev/null +++ b/crates/jose/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "mas-jose" +version = "0.1.0" +authors = ["Quentin Gliech "] +edition = "2021" +license = "Apache-2.0" + +[dependencies] +anyhow = "1.0.51" +async-trait = "0.1.52" +base64ct = { version = "1.0.1", features = ["std"] } +crypto-mac = { version = "0.11.1", features = ["std"] } +digest = "0.9.0" +ecdsa = { version = "0.13.3", features = ["sign", "verify", "pem", "pkcs8"] } +elliptic-curve = { version = "0.11.6", features = ["ecdh", "pem"] } +hmac = "0.11.0" +p256 = { version = "0.10.0", features = ["ecdsa", "pem", "pkcs8"] } +pkcs1 = { version = "0.3.1", features = ["pem", "pkcs8"] } +pkcs8 = { version = "0.8.0", features = ["pem"] } +rand = "0.8.4" +rsa = { git = "https://github.com/sandhose/RSA.git", branch = "bump-pkcs" } +sec1 = "0.2.1" +serde = { version = "1.0.132", features = ["derive"] } +serde_json = "1.0.73" +serde_with = { version = "1.11.0", features = ["base64"] } +sha2 = "0.9.8" +signature = { version = "1.4.0" } +thiserror = "1.0.30" +tokio = { version = "1.15.0", features = ["macros", "rt"] } +url = { version = "2.2.2", features = ["serde"] } +zeroize = "1.4.3" diff --git a/crates/jose/src/iana.rs b/crates/jose/src/iana.rs new file mode 100644 index 00000000..1783cc7e --- /dev/null +++ b/crates/jose/src/iana.rs @@ -0,0 +1,295 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Generated enums from the IANA JOSE registry +//! +//! + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum JsonWebSignatureAlgorithm { + /// HMAC using SHA-256 + #[serde(rename = "HS256")] + Hs256, + + /// HMAC using SHA-384 + #[serde(rename = "HS384")] + Hs384, + + /// HMAC using SHA-512 + #[serde(rename = "HS512")] + Hs512, + + /// RSASSA-PKCS1-v1_5 using SHA-256 + #[serde(rename = "RS256")] + Rs256, + + /// RSASSA-PKCS1-v1_5 using SHA-384 + #[serde(rename = "RS384")] + Rs384, + + /// RSASSA-PKCS1-v1_5 using SHA-512 + #[serde(rename = "RS512")] + Rs512, + + /// ECDSA using P-256 and SHA-256 + #[serde(rename = "ES256")] + Es256, + + /// ECDSA using P-384 and SHA-384 + #[serde(rename = "ES384")] + Es384, + + /// ECDSA using P-521 and SHA-512 + #[serde(rename = "ES512")] + Es512, + + /// RSASSA-PSS using SHA-256 and MGF1 with SHA-256 + #[serde(rename = "PS256")] + Ps256, + + /// RSASSA-PSS using SHA-384 and MGF1 with SHA-384 + #[serde(rename = "PS384")] + Ps384, + + /// RSASSA-PSS using SHA-512 and MGF1 with SHA-512 + #[serde(rename = "PS512")] + Ps512, + + /// No digital signature or MAC performed + #[serde(rename = "none")] + None, + + /// RSAES-PKCS1-v1_5 + #[serde(rename = "RSA1_5")] + Rsa15, + + /// RSAES OAEP using default parameters + #[serde(rename = "RSA-OAEP")] + RsaOaep, + + /// RSAES OAEP using SHA-256 and MGF1 with SHA-256 + #[serde(rename = "RSA-OAEP-256")] + RsaOaep256, + + /// AES Key Wrap using 128-bit key + #[serde(rename = "A128KW")] + A128Kw, + + /// AES Key Wrap using 192-bit key + #[serde(rename = "A192KW")] + A192Kw, + + /// AES Key Wrap using 256-bit key + #[serde(rename = "A256KW")] + A256Kw, + + /// Direct use of a shared symmetric key + #[serde(rename = "dir")] + Dir, + + /// ECDH-ES using Concat KDF + #[serde(rename = "ECDH-ES")] + EcdhEs, + + /// ECDH-ES using Concat KDF and "A128KW" wrapping + #[serde(rename = "ECDH-ES+A128KW")] + EcdhEsA128Kw, + + /// ECDH-ES using Concat KDF and "A192KW" wrapping + #[serde(rename = "ECDH-ES+A192KW")] + EcdhEsA192Kw, + + /// ECDH-ES using Concat KDF and "A256KW" wrapping + #[serde(rename = "ECDH-ES+A256KW")] + EcdhEsA256Kw, + + /// Key wrapping with AES GCM using 128-bit key + #[serde(rename = "A128GCMKW")] + A128Gcmkw, + + /// Key wrapping with AES GCM using 192-bit key + #[serde(rename = "A192GCMKW")] + A192Gcmkw, + + /// Key wrapping with AES GCM using 256-bit key + #[serde(rename = "A256GCMKW")] + A256Gcmkw, + + /// PBES2 with HMAC SHA-256 and "A128KW" wrapping + #[serde(rename = "PBES2-HS256+A128KW")] + Pbes2Hs256A128Kw, + + /// PBES2 with HMAC SHA-384 and "A192KW" wrapping + #[serde(rename = "PBES2-HS384+A192KW")] + Pbes2Hs384A192Kw, + + /// PBES2 with HMAC SHA-512 and "A256KW" wrapping + #[serde(rename = "PBES2-HS512+A256KW")] + Pbes2Hs512A256Kw, + + /// EdDSA signature algorithms + #[serde(rename = "EdDSA")] + EdDsa, + + /// RSA-OAEP using SHA-384 and MGF1 with SHA-384 + #[serde(rename = "RSA-OAEP-384")] + RsaOaep384, + + /// RSA-OAEP using SHA-512 and MGF1 with SHA-512 + #[serde(rename = "RSA-OAEP-512")] + RsaOaep512, + + /// ECDSA using secp256k1 curve and SHA-256 + #[serde(rename = "ES256K")] + Es256K, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum JsonWebEncryptionAlgorithm { + /// AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm + #[serde(rename = "A128CBC-HS256")] + A128CbcHs256, + + /// AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm + #[serde(rename = "A192CBC-HS384")] + A192CbcHs384, + + /// AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm + #[serde(rename = "A256CBC-HS512")] + A256CbcHs512, + + /// AES GCM using 128-bit key + #[serde(rename = "A128GCM")] + A128Gcm, + + /// AES GCM using 192-bit key + #[serde(rename = "A192GCM")] + A192Gcm, + + /// AES GCM using 256-bit key + #[serde(rename = "A256GCM")] + A256Gcm, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum JsonWebEncryptionCompressionAlgorithm { + /// DEFLATE + #[serde(rename = "DEF")] + Def, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum JsonWebKeyType { + /// Elliptic Curve + #[serde(rename = "EC")] + Ec, + + /// RSA + #[serde(rename = "RSA")] + Rsa, + + /// Octet sequence + #[serde(rename = "oct")] + Oct, + + /// Octet string key pairs + #[serde(rename = "OKP")] + Okp, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum JsonWebKeyEcEllipticCurve { + /// P-256 Curve + #[serde(rename = "P-256")] + P256, + + /// P-384 Curve + #[serde(rename = "P-384")] + P384, + + /// P-521 Curve + #[serde(rename = "P-521")] + P521, + + /// SECG secp256k1 curve + #[serde(rename = "secp256k1")] + Secp256K1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum JsonWebKeyOkpEllipticCurve { + /// Ed25519 signature algorithm key pairs + #[serde(rename = "Ed25519")] + Ed25519, + + /// Ed448 signature algorithm key pairs + #[serde(rename = "Ed448")] + Ed448, + + /// X25519 function key pairs + #[serde(rename = "X25519")] + X25519, + + /// X448 function key pairs + #[serde(rename = "X448")] + X448, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum JsonWebKeyUse { + /// Digital Signature or MAC + #[serde(rename = "sig")] + Sig, + + /// Encryption + #[serde(rename = "enc")] + Enc, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum JsonWebKeyOperation { + /// Compute digital signature or MAC + #[serde(rename = "sign")] + Sign, + + /// Verify digital signature or MAC + #[serde(rename = "verify")] + Verify, + + /// Encrypt content + #[serde(rename = "encrypt")] + Encrypt, + + /// Decrypt content and validate decryption, if applicable + #[serde(rename = "decrypt")] + Decrypt, + + /// Encrypt key + #[serde(rename = "wrapKey")] + WrapKey, + + /// Decrypt key and validate decryption, if applicable + #[serde(rename = "unwrapKey")] + UnwrapKey, + + /// Derive key + #[serde(rename = "deriveKey")] + DeriveKey, + + /// Derive bits not to be used as a key + #[serde(rename = "deriveBits")] + DeriveBits, +} diff --git a/crates/jose/src/jwk.rs b/crates/jose/src/jwk.rs new file mode 100644 index 00000000..afe6719c --- /dev/null +++ b/crates/jose/src/jwk.rs @@ -0,0 +1,364 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Ref: + +use anyhow::bail; +use p256::NistP256; +use rsa::{BigUint, PublicKeyParts}; +use serde::{Deserialize, Serialize}; +use serde_with::{ + base64::{Base64, Standard, UrlSafe}, + formats::{Padded, Unpadded}, + serde_as, skip_serializing_none, +}; +use url::Url; + +use crate::iana::{ + JsonWebKeyEcEllipticCurve, JsonWebKeyOkpEllipticCurve, JsonWebKeyOperation, JsonWebKeyUse, + JsonWebSignatureAlgorithm, +}; + +#[serde_as] +#[skip_serializing_none] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct JsonWebKey { + #[serde(flatten)] + parameters: JsonWebKeyParameters, + + #[serde(default)] + r#use: Option, + + #[serde(default)] + key_ops: Option>, + + #[serde(default)] + alg: Option, + + #[serde(default)] + kid: Option, + + #[serde(default)] + x5u: Option, + + #[serde(default)] + #[serde_as(as = "Option>>")] + x5c: Option>>, + + #[serde(default)] + #[serde_as(as = "Option>")] + x5t: Option>, + + #[serde(default, rename = "x5t#S256")] + #[serde_as(as = "Option>")] + x5t_s256: Option>, +} + +impl JsonWebKey { + #[must_use] + pub fn new(parameters: JsonWebKeyParameters) -> Self { + Self { + parameters, + r#use: None, + key_ops: None, + alg: None, + kid: None, + x5u: None, + x5c: None, + x5t: None, + x5t_s256: None, + } + } + + #[must_use] + pub fn with_use(mut self, value: JsonWebKeyUse) -> Self { + self.r#use = Some(value); + self + } + + #[must_use] + pub fn with_key_ops(mut self, key_ops: Vec) -> Self { + self.key_ops = Some(key_ops); + self + } + + #[must_use] + pub fn with_alg(mut self, alg: JsonWebSignatureAlgorithm) -> Self { + self.alg = Some(alg); + self + } + + #[must_use] + pub fn with_kid(mut self, kid: impl Into) -> Self { + self.kid = Some(kid.into()); + self + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct JsonWebKeySet { + keys: Vec, +} + +impl JsonWebKeySet { + #[must_use] + pub fn new(keys: Vec) -> Self { + Self { keys } + } +} + +#[serde_as] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kty")] +pub enum JsonWebKeyParameters { + #[serde(rename = "RSA")] + Rsa { + #[serde_as(as = "Base64")] + n: Vec, + #[serde_as(as = "Base64")] + e: Vec, + }, + #[serde(rename = "EC")] + Ec { + crv: JsonWebKeyEcEllipticCurve, + #[serde_as(as = "Base64")] + x: Vec, + #[serde_as(as = "Base64")] + y: Vec, + }, + #[serde(rename = "OKP")] + Okp { + crv: JsonWebKeyOkpEllipticCurve, + #[serde_as(as = "Base64")] + x: Vec, + }, +} + +impl TryFrom for ecdsa::VerifyingKey { + type Error = anyhow::Error; + + fn try_from(params: JsonWebKeyParameters) -> Result { + let (x, y): ([u8; 32], [u8; 32]) = match params { + JsonWebKeyParameters::Ec { + x, + y, + crv: JsonWebKeyEcEllipticCurve::P256, + } => ( + x.try_into() + .map_err(|_| anyhow::anyhow!("invalid curve parameter x"))?, + y.try_into() + .map_err(|_| anyhow::anyhow!("invalid curve parameter y"))?, + ), + _ => bail!("Wrong key type"), + }; + + let point = sec1::EncodedPoint::from_affine_coordinates(&x.into(), &y.into(), false); + let key = ecdsa::VerifyingKey::from_encoded_point(&point)?; + Ok(key) + } +} + +impl From> for JsonWebKeyParameters { + fn from(key: ecdsa::VerifyingKey) -> Self { + let points = key.to_encoded_point(false); + JsonWebKeyParameters::Ec { + x: points.x().unwrap().to_vec(), + y: points.y().unwrap().to_vec(), + crv: JsonWebKeyEcEllipticCurve::P256, + } + } +} + +impl TryFrom for rsa::RsaPublicKey { + type Error = anyhow::Error; + + fn try_from(params: JsonWebKeyParameters) -> Result { + let (n, e) = match ¶ms { + JsonWebKeyParameters::Rsa { n, e } => (n, e), + _ => bail!("Wrong key type"), + }; + let n = BigUint::from_bytes_be(n); + let e = BigUint::from_bytes_be(e); + Ok(rsa::RsaPublicKey::new(n, e)?) + } +} + +impl From for JsonWebKeyParameters { + fn from(key: rsa::RsaPublicKey) -> Self { + JsonWebKeyParameters::Rsa { + n: key.n().to_bytes_be(), + e: key.e().to_bytes_be(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_google_keys() { + let jwks = r#"{ + "keys": [ + { + "alg": "RS256", + "kty": "RSA", + "n": "tCwhHOxX_ylh5kVwfVqW7QIBTIsPjkjCjVCppDrynuF_3msEdtEaG64eJUz84ODFNMCC0BQ57G7wrKQVWkdSDxWUEqGk2BixBiHJRWZdofz1WOBTdPVicvHW5Zl_aIt7uXWMdOp_SODw-O2y2f05EqbFWFnR2-1y9K8KbiOp82CD72ny1Jbb_3PxTs2Z0F4ECAtTzpDteaJtjeeueRjr7040JAjQ-5fpL5D1g8x14LJyVIo-FL_y94NPFbMp7UCi69CIfVHXFO8WYFz949og-47mWRrID5lS4zpx-QLuvNhUb_lSqmylUdQB3HpRdOcYdj3xwy4MHJuu7tTaf0AmCQ", + "use": "sig", + "kid": "d98f49bc6ca4581eae8dfadd494fce10ea23aab0", + "e": "AQAB" + }, + { + "use": "sig", + "kty": "RSA", + "kid": "03e84aed4ef4431014e8617567864c4efaaaede9", + "n": "ma2uRyBeSEOatGuDpCiV9oIxlDWix_KypDYuhQfEzqi_BiF4fV266OWfyjcABbam59aJMNvOnKW3u_eZM-PhMCBij5MZ-vcBJ4GfxDJeKSn-GP_dJ09rpDcILh8HaWAnPmMoi4DC0nrfE241wPISvZaaZnGHkOrfN_EnA5DligLgVUbrA5rJhQ1aSEQO_gf1raEOW3DZ_ACU3qhtgO0ZBG3a5h7BPiRs2sXqb2UCmBBgwyvYLDebnpE7AotF6_xBIlR-Cykdap3GHVMXhrIpvU195HF30ZoBU4dMd-AeG6HgRt4Cqy1moGoDgMQfbmQ48Hlunv9_Vi2e2CLvYECcBw", + "e": "AQAB", + "alg": "RS256" + } + ] + }"#; + + let jwks: JsonWebKeySet = serde_json::from_str(jwks).unwrap(); + // Both keys are RSA public keys + for jwk in jwks.keys { + rsa::RsaPublicKey::try_from(jwk.parameters).unwrap(); + } + } + + #[allow(clippy::too_many_lines)] + #[test] + fn load_keycloak_keys() { + let jwks = r#"{ + "keys": [ + { + "kid": "SuGUPE9Sr-1Gha2NLse33r5NQu3XoS_I3Qds3bcmfQE", + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "n": "j21ih2m1RPeTXtIPFas2ZclhW8v2RitLdXJTqOFviWonaSObUWNZUkVvIdDKDyJhU7caGPnz52zXX1Trhbbq1uoCalAuIPw9UgJUJhUhlH7lqaRtYdbOrOzXZ7kVsApe1OdlezgShnyMhW5ChEJXQrCkR_LktBJQ8-6ZBNLHx3ps-pQrpXky_XdYZM_I_f1R8z36gnXagklAMMNKciFRURBMAsPbOgaly-slEDdVcuNtcoccSYdo9kRS5wjQlK6LZ3lniJrLRkUMvN6ZQcMLUWMDpghH5bdbhaaOb28HQWwpRDEBIMIH9Fi9aiKxwHa5YAqW1yetOq_9XXyYiuP9G6hZozSnkkfAOzYFqfr92vIPHddVVUUVLvH8UL4u1o553uVtOExA_pJVRghfO0IPZhJ6rUaZR7krvUMdCYngGznuD_V2-TAL9Nu8YXHIrZSU4WBKIvQC2HDOogSjj5dNDBUuAmOhI2OjuLjiOXpRPlaGcMIIlLALwQ76gFTEhTDlRXar7oLU8wj1KHLkc6d__lwdBkR-2Fr4dAewW4bHVFsPeDSM_vJZpK0XACrNgrrNBax48_hOlK9YfzSopyVCHwewxmC743eNYWEhE9LY-cc3ZGK9tHXgQG2l1tOZ_JK9wo1HsIuu3gdl2SV3ZOs6Ggi812GMfrgijnthC7e4Mv8", + "e": "AQAB", + "x5c": [ + "MIIElTCCAn0CBgF95wE6HzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNDE3WhcNMzExMjIzMTExNTU3WjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCPbWKHabVE95Ne0g8VqzZlyWFby/ZGK0t1clOo4W+JaidpI5tRY1lSRW8h0MoPImFTtxoY+fPnbNdfVOuFturW6gJqUC4g/D1SAlQmFSGUfuWppG1h1s6s7NdnuRWwCl7U52V7OBKGfIyFbkKEQldCsKRH8uS0ElDz7pkE0sfHemz6lCuleTL9d1hkz8j9/VHzPfqCddqCSUAww0pyIVFREEwCw9s6BqXL6yUQN1Vy421yhxxJh2j2RFLnCNCUrotneWeImstGRQy83plBwwtRYwOmCEflt1uFpo5vbwdBbClEMQEgwgf0WL1qIrHAdrlgCpbXJ606r/1dfJiK4/0bqFmjNKeSR8A7NgWp+v3a8g8d11VVRRUu8fxQvi7Wjnne5W04TED+klVGCF87Qg9mEnqtRplHuSu9Qx0JieAbOe4P9Xb5MAv027xhccitlJThYEoi9ALYcM6iBKOPl00MFS4CY6EjY6O4uOI5elE+VoZwwgiUsAvBDvqAVMSFMOVFdqvugtTzCPUocuRzp3/+XB0GRH7YWvh0B7BbhsdUWw94NIz+8lmkrRcAKs2Cus0FrHjz+E6Ur1h/NKinJUIfB7DGYLvjd41hYSET0tj5xzdkYr20deBAbaXW05n8kr3CjUewi67eB2XZJXdk6zoaCLzXYYx+uCKOe2ELt7gy/wIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQB+mzE9ZA/hX/GAM74ZXs+ZEjV+qzUsGNpHkXyzdRc1ic28Go5ujAIMxwwsJ4PUSmw6MjPpCKV3kSXoyc7kUDZ/NQ7gwanP4DN8wDq7GLGqT3QRzMLfVy+el2Vjwd3Q6BhXNAK/jPzv0DFu1GG4WCpc1PcM8p/zWkbKWf9u4nBl7RBsMddn7KLxV+D2Y2eGshZ81YVaJiKF9y+gpgyxBOOsTFITu8SxBpXSwBIP4jTv7NllicxI8G9mk87XX3DdA+NHPKsKj35RbDAXyMid8tMl4R3IQ34F3ADuquHpdAdfTNDSm5lwilyWjV35O+8mKA2n/3LAhfCNgxMU0m9Jm8kI/pu9qTXnIx+HMr8IsAMseGxl+dZ/jJjGGPw1VZhHhU78dN+DZlUSKOVjOSQF+8CGuCxMnOx7+leGafs6G6LtsF/vQvJBTB9DRlM3ag0hQRT2ZEXPWSvcz3ARXqWyaHTzhR4F/+rRX1CyBsCdG3b3iicjGp7EPeaqXEki1K3SNwwv1byeJfqP785auswpojpUYfp/J850VAfA4xuVvxK3xuJrvbpS4DR6JQPY0fs6g8JEDahYa6rSB8H9toLC2r92gerqcGFpEU8uHRHxm9QZjIyFh78LWqpfegz0HMjYqaULgZJxqqZH2sVIu+nPuKC7tIjYWtODR0A13Ar3lH8aZg==" + ], + "x5t": "fvgfH2gggONL7t4ZTvOdBpI94kM", + "x5t#S256": "uwHwO2crQ74jak2bmAeAt_4nrqGDQoElaiVvOlSGOOw" + }, + { + "kid": "7pW7bkOM27LQ-KJGHzT1dt3yBmhcj20xj7A-itsuY6U", + "kty": "RSA", + "alg": "RS384", + "use": "sig", + "n": "lI1actdwWsMY8BpY68x8No7fwokLTTcZ8-qpqF9CDwX40X70ql9JPqTpLAHp7H7byfO-8VqZVKYKdzFCLjaEqs6Vx6YYuu4BsM2RIDI2CmClngUE5RMXnaEj8XP-h8Q4FnGcXL47n2UNr9mbZSp85W0TWOLtMczuqwwJ2jcYkDFtvLY0UirioKzN5Vr29WdDiCm9i4jHvHE7W41LFCOFLOLxGOq9wLVRNRMRcC3YS6WlrfiMFkPQIGxzFH2OiW2iR9x8QHmxqrqdfidmFsosgG5_2tbX3Q5PnHjYTNHh-iY4uIQ6bsBj1Enoj5h5kudwtgHDyn9OAiljTqLMXsoK9KEZrjE8zPnxQtvfXLCby2CI69X5JZ2lQJCch4cn1eIxn-jJ9Z0aE9EML1Bfp6w5sKELXt1aRtu5HQ5IQ__y2sBJd91NdiBxAzCK5kZjhRIRtt57J5ZHTLsBeHvr2L7SwZ_FojrQly7mI5PMGthZoGoVAr-bJcInzICpcsLKWdW-C6jxhXwRtnJOuTizEOr33vnLohMlmJUZiomYnKv8MEFAmihK5GAHTJ-4QIUuUeC13Dl5aRJacxvoKfgR_zw9P6HCUb7Nq7uzN3oqUdmDYYng1OFVo-1liYuCLbH6ep5LTmAstQY3IjkIFKeY-tvSPdpC9y1TwaHqEktXckvRGx0", + "e": "AQAB", + "x5c": [ + "MIIElTCCAn0CBgF95wGjLjANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNDQzWhcNMzExMjIzMTExNjIzWjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCUjVpy13BawxjwGljrzHw2jt/CiQtNNxnz6qmoX0IPBfjRfvSqX0k+pOksAensftvJ877xWplUpgp3MUIuNoSqzpXHphi67gGwzZEgMjYKYKWeBQTlExedoSPxc/6HxDgWcZxcvjufZQ2v2ZtlKnzlbRNY4u0xzO6rDAnaNxiQMW28tjRSKuKgrM3lWvb1Z0OIKb2LiMe8cTtbjUsUI4Us4vEY6r3AtVE1ExFwLdhLpaWt+IwWQ9AgbHMUfY6JbaJH3HxAebGqup1+J2YWyiyAbn/a1tfdDk+ceNhM0eH6Jji4hDpuwGPUSeiPmHmS53C2AcPKf04CKWNOosxeygr0oRmuMTzM+fFC299csJvLYIjr1fklnaVAkJyHhyfV4jGf6Mn1nRoT0QwvUF+nrDmwoQte3VpG27kdDkhD//LawEl33U12IHEDMIrmRmOFEhG23nsnlkdMuwF4e+vYvtLBn8WiOtCXLuYjk8wa2FmgahUCv5slwifMgKlywspZ1b4LqPGFfBG2ck65OLMQ6vfe+cuiEyWYlRmKiZicq/wwQUCaKErkYAdMn7hAhS5R4LXcOXlpElpzG+gp+BH/PD0/ocJRvs2ru7M3eipR2YNhieDU4VWj7WWJi4Itsfp6nktOYCy1BjciOQgUp5j629I92kL3LVPBoeoSS1dyS9EbHQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCBKgIXOSH8cLgKHq1Q5Zn69YdVpC8W8gp3hfjqa9lpER8MHyZVw0isOzdICrNZdgsatq/uaYBMkc3LwxDRWJVN8AmKabqy6UDlAwHf7IUJJMcu3ODG+2tsy3U1SGIVWIffpOfv3F/gXxU76IXWnHiUzjCMYnWJg0Oy0G2oCDHk/7h82Dmq688UmPW+ycZhktjZ8lXqlopVZhjssTa48xJjtDdwN8OVmPGpV/uVzlDTCuYbyVWTYrEfnKwwVhzmAoIYc4XxDKZQ/z1zqE3HtIGrems7lGpgry55JMIRSYxoD2gg2YscDvuCnfzITwTPjijuyI7ocP6eA13FHriIcfHYEzKENUoEgWeybgs09JyIp3yE7YelL94vY4xJRVeL1jMmP5Wi6pM9cMKgQwkUzq7tmupkh9c6jF+tPStByDvD11ybJi5A/S2Rmer2qhlgnsml4NHkMZgIcWtokxoGmXoMcz6AOx31nRvvBHjC2emVnUmzojTCc5mPY3TRgzlAb+cQE/JIreZMfhfLwk4ny5dq+r4ya02fo7BrDA8oJJAP0gC82KNW5aZVpZSbkeRdogTVWdmiNYxvq95gI4ijLneYwSgWb1PM+CRhlNY7neJEv0VT5fbMd0XQZnxzSzQVymPiBHMEJBUul6UuxjVlJb7cdCtIty0zEWO3/uaEzqQl3w==" + ], + "x5t": "Fk9zR2uLwBS6fHJbxM08TjDhUi8", + "x5t#S256": "ZiBGLQCaqehbgYF5A2dicp7WaL-zE4UTbFYyHKXDU_o" + }, + { + "kid": "Jnf5fTyMpeiUyJnc3PHJaM9pR6VjWejv9RVyJgPugFs", + "kty": "RSA", + "alg": "RS512", + "use": "sig", + "n": "m3Y_aeHLL00X-bBPF3ySQ5ebOQ0dz40IQ4uWwWzL59zxn1AwzqrfrfAkKt_RJvJycfmy4zFeu89bNI86r6PtQVSvLqRYKo9UI4Y5jXs4HyvGvSL-DOXl8b8ybpo-o3bEiTgGOvIw2NGv49xT-_3SJ4Rba6awqVxkj334eZunrfvwYG9bjbAgPqWgMcuLVQNdNpytRHMB8Cjnd0SouL1dVxHlgHpYsZcRbsTsvPO1fRHcQRel44CgQRCZ08BvgETrF_9eATiRKBz18XbhaCZfSqh3a7IA-w9e236w6oD4ATOigeMHYZ0sfqKeoCsSd4rQ9kVc-U_EtL73_BVV7pmM4Xcl8JB8vzi_FMQVotzj5SgawylIxRdWUOGjyVFcUJ_u-DikoneVway0T4fXFJkWUflIoqf5-lHmMupb32q0E_pNL728yOlBfqm3bfJF9SF9w-h2SFMHWdRUzVOrtDRdrJVReGPPWvUHByALLL6B33FEcHDIcw4wqSfEmD6ypYJQxX8Er3_X9QFCgkn_rYUitUx90jOZ0n5vhubYnhiXX3RpeOCh9gF2O3h9Tv-DrynUO6OOgUSsBBbI-tGC5ebT51P0IJRkK3i4TkIYZnv7lj2auGWMC0-o7w24k_fG4U0EAr9N2cenR3Pepl6pjTa2g3y3C5_0LDUrcd67QPKl6ZE", + "e": "AQAB", + "x5c": [ + "MIIElTCCAn0CBgF95wHdoDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNDU4WhcNMzExMjIzMTExNjM4WjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCbdj9p4csvTRf5sE8XfJJDl5s5DR3PjQhDi5bBbMvn3PGfUDDOqt+t8CQq39Em8nJx+bLjMV67z1s0jzqvo+1BVK8upFgqj1QjhjmNezgfK8a9Iv4M5eXxvzJumj6jdsSJOAY68jDY0a/j3FP7/dInhFtrprCpXGSPffh5m6et+/Bgb1uNsCA+paAxy4tVA102nK1EcwHwKOd3RKi4vV1XEeWAelixlxFuxOy887V9EdxBF6XjgKBBEJnTwG+AROsX/14BOJEoHPXxduFoJl9KqHdrsgD7D17bfrDqgPgBM6KB4wdhnSx+op6gKxJ3itD2RVz5T8S0vvf8FVXumYzhdyXwkHy/OL8UxBWi3OPlKBrDKUjFF1ZQ4aPJUVxQn+74OKSid5XBrLRPh9cUmRZR+Uiip/n6UeYy6lvfarQT+k0vvbzI6UF+qbdt8kX1IX3D6HZIUwdZ1FTNU6u0NF2slVF4Y89a9QcHIAssvoHfcURwcMhzDjCpJ8SYPrKlglDFfwSvf9f1AUKCSf+thSK1TH3SM5nSfm+G5tieGJdfdGl44KH2AXY7eH1O/4OvKdQ7o46BRKwEFsj60YLl5tPnU/QglGQreLhOQhhme/uWPZq4ZYwLT6jvDbiT98bhTQQCv03Zx6dHc96mXqmNNraDfLcLn/QsNStx3rtA8qXpkQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAf2H6GjobSvc50L+cXeizzG6rg6Sm3x31PB7AH7XlVI+cytWA0X04IhuX+9H2VdEqujSApY/WM9voneyEm1eC3L6p4StO7icB+H4GYctzY+KV0qlbH3iMQkz+xngOTaEj+c9lSZlG7FSlL7Eybjjlj9mLyNJv4aiW7lQCxTWu7RcFq+w2ogvR7iv4uwbY9SHO/Fs5qbwzNIP65W9abcZvEAZKXQ69jOZ01VhNqiIA2D0OstjLWTfGaO0WxrUxvBVRqB3a86qIIwHjatrqdoGasLLGz8bAU3rY2b/DwZ7VBljUuZ+7PlysSK3w22k6eQe5G+XgxSl4Mzn+6lzCdoXeSVUzvQZrk+JBaDTVN5V5fteHSjLcaGNwIg9qYOHdx7PBYhbHP/hXADSQH90xIMipG168NOGBaxw+ybCaD6Eg+PfsPGnXO0Wnnd0PN/Dz4LggTLBwlbWaIDltj++0Xxlf375MrK1A9mDkhcdAOzZtkBkTD9UeXqL6UD0R0CFHp0B+TQEZuOuKRMKmlA2eo8f8z70vGToYk5TW/lvi8Li44+Y7UGLlLirpOtfBI35TPLK0OGfLh1dfqnuFQACObk+Ia+ON//r203sSQYQf3Qcq7u5KC/S406W+dSJ+c7Cf+8piMVc42PhYemdrkEPgzuTmzTJga2HFQk8BCUwoL1euMdw==" + ], + "x5t": "bPku6_PBAoke1DpEcT0ghZYp6Fc", + "x5t#S256": "kIo7Hxj-A4jrwOBfo87c2kmAZzs87OHSd8tS4s_PGgk" + }, + { + "kid": "WerdZfF_9ZgxLyHepk92CsKAEubvCs3rIAAy6wrUZUc", + "kty": "RSA", + "alg": "PS256", + "use": "sig", + "n": "85fgcXq_tB48BI8oeF9gjeWqL1opGtHoXv4rmwaxwfwzFU2ywJWRIEjwcJ_ypMPdC1im_kz_VCqWZBFyXfpuaEFkcsIAlLLnklI2TPUD3SV5taV_TXA61fm59K59iJDJr9EaQ_j5WJRGRluJpAi_q55U1vBWAHtnweL9RveQ-Ykc_qhpCcGDIek3-tAvJtVCpKQb764tkvmBD3pUPYTdVKHW4TAp4wFcgcj78E-xWELfm0T1nr7kZu-mV9DGYBZhFIWkf0lm4KA6NVDwWe-d1k-20FpT1tNsugK2Zx7SX2N5ytM2bCLH88Fcphvh9Bw_t7GgtZ9PvihJXdJcHR8nqlCsRMsGpeS6tnEl4E8StcTccgOkw1n2FJ-xxLM9eMOcfY--B9eKSaLRjLrhvWfa5-MGpB5JFrB4Rv17SD02Uoz1lwogCXPzTbKkBJhiA-YDinTRyGzyHTNXWsrmOLXrVRXUqdNYG32mpy1m3cSpoz9fOWne2dKKj9eawxFHa-GCzdfX3JBfgVKGGgaL5E_HlkJxx9OHNfQQQ4_OjyzqQGCoPG7jDCn9svb7hOE2epmYywShCgCsL_DZmTm3OdVWMLZ6oi77SIytWSx8QDy5KNCx3YsSLDg7sWv6t58gerWv1gkjhFzhyi3mqsw53WkeUyInrLoDYzEPkjWv3kSKQeM", + "e": "AQAB", + "x5c": [ + "MIIElTCCAn0CBgF95wIdDjANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNTE1WhcNMzExMjIzMTExNjU1WjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDzl+Bxer+0HjwEjyh4X2CN5aovWika0ehe/iubBrHB/DMVTbLAlZEgSPBwn/Kkw90LWKb+TP9UKpZkEXJd+m5oQWRywgCUsueSUjZM9QPdJXm1pX9NcDrV+bn0rn2IkMmv0RpD+PlYlEZGW4mkCL+rnlTW8FYAe2fB4v1G95D5iRz+qGkJwYMh6Tf60C8m1UKkpBvvri2S+YEPelQ9hN1UodbhMCnjAVyByPvwT7FYQt+bRPWevuRm76ZX0MZgFmEUhaR/SWbgoDo1UPBZ753WT7bQWlPW02y6ArZnHtJfY3nK0zZsIsfzwVymG+H0HD+3saC1n0++KEld0lwdHyeqUKxEywal5Lq2cSXgTxK1xNxyA6TDWfYUn7HEsz14w5x9j74H14pJotGMuuG9Z9rn4wakHkkWsHhG/XtIPTZSjPWXCiAJc/NNsqQEmGID5gOKdNHIbPIdM1dayuY4tetVFdSp01gbfaanLWbdxKmjP185ad7Z0oqP15rDEUdr4YLN19fckF+BUoYaBovkT8eWQnHH04c19BBDj86PLOpAYKg8buMMKf2y9vuE4TZ6mZjLBKEKAKwv8NmZObc51VYwtnqiLvtIjK1ZLHxAPLko0LHdixIsODuxa/q3nyB6ta/WCSOEXOHKLeaqzDndaR5TIiesugNjMQ+SNa/eRIpB4wIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQDtW7hL3dWY0Nu87SkAPweBLocyI/S2/XZogBByzqdEWZru+26xQoUacqgYbrmQ6frQfWwlfpuzp7HBheDAHVobjlhl2jUQ7xO5vzTiB1bd/X1cQgOdTHosqiyTXLRBJKr3GQyfjrS3ruWKScGg5Y4jYGbsAoO3cNProddFeLbak0aQXGkhyWib2CzqtIpBA9Zy7EJYIWd5O+tExNIv+mjhSZZ6s3qdWXo/4RkVzBeGx5PApdoI/B7y0vwg4Dlt8qB9JcV9WL4nzI4s8foPMXuXgg+HJllB+NkSnTlQj77oU3pbrBoYgVhEdbfYkQuIdwYOWBQi/hdmV0YjUQQTAjYKBFKWQWCoAVKnfMpbDkdjN8KhOzohZ7KEahvHsnFt/PnS5MlFseZN9e6k4MB96EQ4fem7n/sPx4zqvhZMrPCaUT616hfCTa3DPoHzi2CxebE7GE95veQOtk9jCsXEbqKPvZ83/dfz5ftWu5wGHnhIK9S5sCCgjo2RA8bCLBl6/tBpmE0BwWqQqSZEs4zyXTplko822aJyxJtYprmDK0Ktxm6IEjSpEDCLuirnpQ0+Z8w19Key58Kx+OhNHczJK9wEaygKBQC1vvPV8ZvcHOx4XJgL8QwbPhaR5706YRfXTBceK2aw+oWzoNLJ4X7B2LB9IA84pJZKW+VfmnBz52iiqw==" + ], + "x5t": "Xdy9viGu6isFknWeThJbh2_r4Qo", + "x5t#S256": "-toFY0ysJ3uopRPDNIQBo2VV_XT5YkniW2I-6XK_2oc" + }, + { + "kid": "JjGFU4NwBkjaNRmIEw5BpoggXtG8dsl4s7gs29eYvno", + "kty": "RSA", + "alg": "PS384", + "use": "sig", + "n": "h3RNtfVqZPTQuFYBN54gOgcLX7bK-3qUyXstFso_V09RCHLHbFZV_czEC30lRQ6U5QeZ7iFpu7GbiM1csBk4HqhQ2v0TnjlQxIv9-71VV1JPZHrKsDFZlSr4HlZhkt6myBH16aDBT56U8pKg4oAVkoYS4dpzsR0q30zzrKAMgHRLTYWbCaGGGa1BuEUF9WgUhVnuiMu4ay9Tv0auu1UsdTkXjdR2YcWv2AihvFb4xYUSMBQr0bvUeMF_AAJ0B0VrGWIb51nARO2PNimKviHnFTrlaOyFJsnzwiiijuaOx2HZMQfcObzTz4Hx_YYIexOS83bYYkyGgvgUdu0wqls7ChgaZ_qiQdNnr_RWahIN2iVhjyOJuqsFsXufvHYo0nB1BFm1gnDHgYXdJIrSPql4g9gh1NZD_P0PuniPq3jvPoiQJ2u_9a8RDe9Scb_KzRgrBk0tkaXELDw1Q7ccJx9HUUbTxNkzNtZ6Z4MiKT4n0Bx4joglnL1BXvM5yrlO89brXAmfZgx6OmH7Dractz_Bny6QUHwF5vLMhMuVXsC5dU6UbkSZq82S5SnwnLHAe4JBOC-FTB08wAKgQXat16MIqmrBuKVtdNSshAxMk0wd_jPe-G4A_2RJ6pXSrQOkUFNPrOfV_PQMqI92zCYbIByWEwdfQAkavR2HC0-iDd202NM", + "e": "AQAB", + "x5c": [ + "MIIElTCCAn0CBgF95wJajzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNTMwWhcNMzExMjIzMTExNzEwWjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCHdE219Wpk9NC4VgE3niA6Bwtftsr7epTJey0Wyj9XT1EIcsdsVlX9zMQLfSVFDpTlB5nuIWm7sZuIzVywGTgeqFDa/ROeOVDEi/37vVVXUk9kesqwMVmVKvgeVmGS3qbIEfXpoMFPnpTykqDigBWShhLh2nOxHSrfTPOsoAyAdEtNhZsJoYYZrUG4RQX1aBSFWe6Iy7hrL1O/Rq67VSx1OReN1HZhxa/YCKG8VvjFhRIwFCvRu9R4wX8AAnQHRWsZYhvnWcBE7Y82KYq+IecVOuVo7IUmyfPCKKKO5o7HYdkxB9w5vNPPgfH9hgh7E5LzdthiTIaC+BR27TCqWzsKGBpn+qJB02ev9FZqEg3aJWGPI4m6qwWxe5+8dijScHUEWbWCcMeBhd0kitI+qXiD2CHU1kP8/Q+6eI+reO8+iJAna7/1rxEN71Jxv8rNGCsGTS2RpcQsPDVDtxwnH0dRRtPE2TM21npngyIpPifQHHiOiCWcvUFe8znKuU7z1utcCZ9mDHo6YfsOtpy3P8GfLpBQfAXm8syEy5VewLl1TpRuRJmrzZLlKfCcscB7gkE4L4VMHTzAAqBBdq3XowiqasG4pW101KyEDEyTTB3+M974bgD/ZEnqldKtA6RQU0+s59X89Ayoj3bMJhsgHJYTB19ACRq9HYcLT6IN3bTY0wIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAoudkN4cTAnT2b7cd/JklLFLBnw+mwSgj0ZYyRByBiC0AXU+LmM+D1Bs0TRqXKICBZ2dxKRr8Z1PdQe8BghWcl84iLXEjHVdw08/xVaQ5GKcGLOfSRG+3Suj6UyZfwcMJtX4GO919fX10mAlk6ySHe6SViSVMup5ePwA0C7Jws9/aXNLIvw82hIX8IVM1kuuu3DICQlr1nsvbu6XVQT5kdhIpApr+IDrBvNFWKPdH+vA8Kxb8wkhk9HIUbAi3WqftHoiI8Qq92BYcB5gjwocAkzmrDDoAulEM24+IJoK87DWEeC0Vu9kOB7i5PKXqUANJ7ebQJJhgXy+xNq1Alh4f95mqolXCxdo1jJi/OFExLDr93Fk5QVRQxi2aSEDkoz/h7stzuUvvTyT75pJAILSL+xv8Gd/bYhL5lfCXcHA9uPDQwM/9gnA1ojIdF1bvgaEo2r1xoY/LAScTB0nzvRh1EVoZYxBHid+79MJWQq0vpJ58pyKcxgKaoD1pUQ2brAlYFNflNiMN18VnCF7vnY8Ol9Po881ee2TWLex0i5cLREo4fvPNg0QgoaQvDqlvJqr1nJll/Mzv2w9s3agQxPwKRkTOTb4jNOV23Uy0SbxBD42EOllLmUN897ra3pdmacHHMatw75Sfcu4WhuMrN13RzVUARMjFN+nNI8i7ay9WJOA==" + ], + "x5t": "4ovci1k_HPeLoL2PhUrmoDlLQhU", + "x5t#S256": "PJsKbXoQ7tZoR7aRDli60V65BPtO-Q7QSpk5P5hDcLY" + }, + { + "kid": "zesnP0SwjgVGBU5RPhqccF0W4BbMkbwtZpjAeTAgwz8", + "kty": "RSA", + "alg": "PS512", + "use": "sig", + "n": "x4NHNpmzOgqWgQGsiWTpyhdIkSSiO0hMKr_5oNNecp254CSO_zEPS6wWKMNwwZRteKIPzPafCkXvmGEuQo716CL9OP5T8BR25sXkws0llygfbbSK2dTWVN4lhM1Rm6zFJ4aK0BZo6EXDp0E0Od8SQSN7FooRAWOiO7HvjgpIdRyqkANElBSL7aNdsPP7dgVMua5P6MNfVjKCe93C-iqsOVadUV5UM3oblf6M_KkDV9GNr6oAizfrXHpPnHjG29u-DSsmCbLimgZaJ3LDnLrmzxbbl9b4mHJQqe00rNDUF6Q6BmmDgJGDMdPH4J8i6w_1z4Xll8Ul-UGHS6rJZeTVsEdKGSOoIbhQa9iuGxC_I_YIjkVbV3O8LcYBzDKetzups4R5CVFpwvAK03UCdM7yLkbDglWcSOYtbPVBafumCzyjWX9u7CpBAcVWe9KpEMVCYgi90TSkX2Vw1bPP07mTBFmK0fwmU2ZlDR0S9Q2NT9St7zWP6teuOeue7PAFlPUVotFdoh8ltZVLEfUTo81E1tiNycDCy9QTP9CzwplqpPIkmTdjmMCO6lollLrTm9SuXGp2FSUdE43tYEzRGNqsGpcwskkvzQWtl7bETaS5vCwPH76k6qGf-TpOHnOH1G7vDzDkewqJ-oscqwkdw4ONo_KxT-CGwv-JwMoSXWEtMKE", + "e": "AQAB", + "x5c": [ + "MIIElTCCAn0CBgF95wKLoDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNTQzWhcNMzExMjIzMTExNzIzWjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDHg0c2mbM6CpaBAayJZOnKF0iRJKI7SEwqv/mg015ynbngJI7/MQ9LrBYow3DBlG14og/M9p8KRe+YYS5CjvXoIv04/lPwFHbmxeTCzSWXKB9ttIrZ1NZU3iWEzVGbrMUnhorQFmjoRcOnQTQ53xJBI3sWihEBY6I7se+OCkh1HKqQA0SUFIvto12w8/t2BUy5rk/ow19WMoJ73cL6Kqw5Vp1RXlQzehuV/oz8qQNX0Y2vqgCLN+tcek+ceMbb274NKyYJsuKaBloncsOcuubPFtuX1viYclCp7TSs0NQXpDoGaYOAkYMx08fgnyLrD/XPheWXxSX5QYdLqsll5NWwR0oZI6ghuFBr2K4bEL8j9giORVtXc7wtxgHMMp63O6mzhHkJUWnC8ArTdQJ0zvIuRsOCVZxI5i1s9UFp+6YLPKNZf27sKkEBxVZ70qkQxUJiCL3RNKRfZXDVs8/TuZMEWYrR/CZTZmUNHRL1DY1P1K3vNY/q1645657s8AWU9RWi0V2iHyW1lUsR9ROjzUTW2I3JwMLL1BM/0LPCmWqk8iSZN2OYwI7qWiWUutOb1K5canYVJR0Tje1gTNEY2qwalzCySS/NBa2XtsRNpLm8LA8fvqTqoZ/5Ok4ec4fUbu8PMOR7Con6ixyrCR3Dg42j8rFP4IbC/4nAyhJdYS0woQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCr+GGCVS/sBHukLZay8WlBXtowJ6qyX8hMFClDGDN9/c3mUbLJsCCVN6Jbr33BgNZ/ZuvLhUvhWGPlOXUB3Rf+qRzNEzoLVwanw2yCUEKFi6AvuBUY9twNnifH4y1Cg34NVaZoPvQ0hlOLGYl9CCxen7VMLJ5QbTC8H3fPX1prWOic5x46Bu7IqoEqZtDszt8F+uteruRsHVHCiWx5dW7goeIa8YsUK0A4mnOy5kViSvs5L6Kq0N5uCB9EDu/Ew5R0/mi/UTm5L8CpzQig1pmvDtIy7ZnosHu7zYGSQiR04jn3Od0rdWzTCcs8W79+ewgJ0bdYmfvSnVehs1BR+cjivzBqMWMqdyz6eQXCy/esiG5KDIxH4F0HGLiiwXqHUYjJPex8TId+fz0MFScrEN5fjE+XltGzsPwlcgnAqE0pN0ExJSHwzBHNkJJQpjHrsEurWn9QGBqD75Vt9yVeHE8MZ4zMGj3ZkRmn1x6wVBdv1V12P3e4b8V5aG02FbREkJzFTXtGyDHtw/hlWGz9M9w0c5TAI6xYPa1gS6/Fw95J6S0V3n3JH+xqi6yv2H2cQHukFxFSPJW1cc/hh5DJ4Ag8+pKuO1Vdo9p+DltaGLWBabON7GZZojlYdx2WtBZK9CMRgrxobg+OBA44AHkiWkhflrqGLYul866wiNu6zLEfdQ==" + ], + "x5t": "0lMdqEAhOWfUXDivtS-KwPvwKNY", + "x5t#S256": "aOjQ1awJmcaF7Yiz75ifjBKbjr4Eo-Ha5uNMi-TtuGw" + }, + { + "kid": "VlsIs1LssBo6r8EuXJo81rDEoTYpUjiMkeq_PlapKfY", + "kty": "EC", + "alg": "ES256", + "use": "sig", + "crv": "P-256", + "x": "3kqy7us0mepJJblWwj0Exg2S7PtWaJvB7SI_ptg0jrA", + "y": "S5Z8d4AfCvRL-hUd6Pv-L3tH6H9T4RIwO2tvBS0hj1A" + }, + { + "kid": "1yWLiqf8sa-em0hSbtZEjKmrardmQdYLR9gpzsypMCU", + "kty": "EC", + "alg": "ES384", + "use": "sig", + "crv": "P-384", + "x": "i4YYGQZd5QQ1JpUXcrZe5wpCid3pqFLnzxxy89Chn-NQ1oYDPTP2M8V9sfazeuB0", + "y": "xf4qN2ZuMLVh4GmRVt1PHhQooB2o61pF0lHrBlIod5hVamiRtUo_Np9PikPD8Uap" + }, + { + "kid": "V5EwcLp9vmwAnstzI1Ndba-iWkX5oTBHK7GnYTyfuOE", + "kty": "EC", + "alg": "ES512", + "use": "sig", + "crv": "P-521", + "x": "rScgdd_n2cHLyzZvP8zw0u9vQyhu0VsbfQypheS7aDoHRLcXccPQTsmrQLrLuKX8PPkITjL_BJDSm7Bo8gv5Sd4", + "y": "Vu3rTFNn_9zWTki95UGT1Bd9PN84KDXmttCrJ1bsYHTWQCaEONk8iwA3U6mEDrg4xtZSTXXKCFdFP13ONWB9oZ4" + } + ] + }"#; + + let jwks: JsonWebKeySet = serde_json::from_str(jwks).unwrap(); + // The first 6 keys are RSA, 7th is P-256 + let mut keys = jwks.keys.into_iter(); + rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap(); + rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap(); + rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap(); + rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap(); + rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap(); + rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap(); + ecdsa::VerifyingKey::try_from(keys.next().unwrap().parameters).unwrap(); + } +} diff --git a/crates/jose/src/jwt.rs b/crates/jose/src/jwt.rs new file mode 100644 index 00000000..57d97f1c --- /dev/null +++ b/crates/jose/src/jwt.rs @@ -0,0 +1,267 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::str::FromStr; + +use base64ct::{Base64UrlUnpadded, Encoding}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_with::{ + base64::{Base64, Standard, UrlSafe}, + formats::{Padded, Unpadded}, + serde_as, skip_serializing_none, +}; +use url::Url; + +use crate::{ + iana::{ + JsonWebEncryptionAlgorithm, JsonWebEncryptionCompressionAlgorithm, + JsonWebSignatureAlgorithm, + }, + jwk::JsonWebKey, + SigningKeystore, VerifyingKeystore, +}; + +#[serde_as] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize)] +pub struct JwtHeader { + alg: JsonWebSignatureAlgorithm, + + #[serde(default)] + enc: Option, + + #[serde(default)] + jku: Option, + + #[serde(default)] + jwk: Option, + + #[serde(default)] + kid: Option, + + #[serde(default)] + x5u: Option, + + #[serde(default)] + #[serde_as(as = "Option>>")] + x5c: Option>>, + + #[serde(default)] + #[serde_as(as = "Option>")] + x5t: Option>, + #[serde(default, rename = "x5t#S256")] + #[serde_as(as = "Option>")] + x5t_s256: Option>, + + #[serde(default)] + typ: Option, + + #[serde(default)] + cty: Option, + + #[serde(default)] + crit: Option>, + + #[serde(default)] + zip: Option, +} + +impl JwtHeader { + pub fn encode(&self) -> anyhow::Result { + let payload = serde_json::to_string(self)?; + let encoded = Base64UrlUnpadded::encode_string(payload.as_bytes()); + Ok(encoded) + } + + #[must_use] + pub fn new(alg: JsonWebSignatureAlgorithm) -> Self { + Self { + alg, + enc: None, + jku: None, + jwk: None, + kid: None, + x5u: None, + x5c: None, + x5t: None, + x5t_s256: None, + typ: None, + cty: None, + crit: None, + zip: None, + } + } + + #[must_use] + pub fn alg(&self) -> JsonWebSignatureAlgorithm { + self.alg + } + + #[must_use] + pub fn kid(&self) -> Option<&str> { + self.kid.as_deref() + } + + #[must_use] + pub fn with_kid(mut self, kid: impl Into) -> Self { + self.kid = Some(kid.into()); + self + } +} + +impl FromStr for JwtHeader { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let decoded = Base64UrlUnpadded::decode_vec(s)?; + let parsed = serde_json::from_slice(&decoded)?; + Ok(parsed) + } +} + +#[derive(Debug)] +pub struct JsonWebTokenParts { + payload: String, + signature: Vec, +} + +impl FromStr for JsonWebTokenParts { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (payload, signature) = s + .rsplit_once('.') + .ok_or_else(|| anyhow::anyhow!("no dots found in JWT"))?; + let signature = Base64UrlUnpadded::decode_vec(signature)?; + let payload = payload.to_owned(); + Ok(Self { payload, signature }) + } +} + +pub struct DecodedJsonWebToken { + header: JwtHeader, + payload: T, +} + +impl DecodedJsonWebToken +where + T: Serialize, +{ + fn serialize(&self) -> anyhow::Result { + let header = serde_json::to_vec(&self.header)?; + let header = Base64UrlUnpadded::encode_string(&header); + let payload = serde_json::to_vec(&self.payload)?; + let payload = Base64UrlUnpadded::encode_string(&payload); + + Ok(format!("{}.{}", header, payload)) + } + + pub async fn sign(&self, store: S) -> anyhow::Result { + let payload = self.serialize()?; + let signature = store.sign(&self.header, payload.as_bytes()).await?; + Ok(JsonWebTokenParts { payload, signature }) + } +} + +impl DecodedJsonWebToken { + pub fn new(header: JwtHeader, payload: T) -> Self { + Self { header, payload } + } + + pub fn claims(&self) -> &T { + &self.payload + } +} + +impl FromStr for DecodedJsonWebToken +where + T: DeserializeOwned, +{ + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (header, payload) = s + .split_once('.') + .ok_or_else(|| anyhow::anyhow!("invalid payload"))?; + + let header = Base64UrlUnpadded::decode_vec(header)?; + let header = serde_json::from_slice(&header)?; + let payload = Base64UrlUnpadded::decode_vec(payload)?; + let payload = serde_json::from_slice(&payload)?; + Ok(Self { header, payload }) + } +} + +impl JsonWebTokenParts { + pub fn decode(&self) -> anyhow::Result> { + let decoded = self.payload.parse()?; + Ok(decoded) + } + + pub async fn verify( + &self, + decoded: &DecodedJsonWebToken, + store: S, + ) -> anyhow::Result<()> { + store + .verify(&decoded.header, self.payload.as_bytes(), &self.signature) + .await?; + + Ok(()) + } + + pub async fn decode_and_verify( + &self, + store: S, + ) -> anyhow::Result> { + let decoded = self.decode()?; + self.verify(&decoded, store).await?; + Ok(decoded) + } + + #[must_use] + pub fn serialize(&self) -> String { + let payload = &self.payload; + let signature = Base64UrlUnpadded::encode_string(&self.signature); + format!("{}.{}", payload, signature) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::SharedSecret; + + #[tokio::test] + async fn decode_hs256() { + let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + let jwt: JsonWebTokenParts = jwt.parse().unwrap(); + let secret = "your-256-bit-secret"; + println!("{:?}", jwt); + let store = SharedSecret::new(&secret); + let jwt: DecodedJsonWebToken = + jwt.decode_and_verify(&store).await.unwrap(); + + assert_eq!(jwt.header.typ, Some("JWT".to_string())); + assert_eq!(jwt.header.alg, JsonWebSignatureAlgorithm::Hs256); + assert_eq!( + jwt.payload, + serde_json::json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1_516_239_022 + }) + ); + } +} diff --git a/crates/jose/src/keystore.rs b/crates/jose/src/keystore.rs new file mode 100644 index 00000000..740bea0c --- /dev/null +++ b/crates/jose/src/keystore.rs @@ -0,0 +1,510 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use anyhow::bail; +use async_trait::async_trait; +use base64ct::{Base64UrlUnpadded, Encoding}; +use digest::Digest; +use ecdsa::VerifyingKey; +use hmac::{Hmac, Mac, NewMac}; +use p256::{NistP256, PublicKey}; +use pkcs1::EncodeRsaPublicKey; +use pkcs8::EncodePublicKey; +use rsa::{PublicKey as _, RsaPublicKey}; +use sha2::{Sha256, Sha384, Sha512}; +use signature::{Signature, Signer, Verifier}; + +use crate::{ + iana::JsonWebSignatureAlgorithm, JsonWebKey, JsonWebKeyOperation, JsonWebKeySet, JwtHeader, +}; + +#[async_trait] +pub trait SigningKeystore { + async fn prepare_header(self, alg: JsonWebSignatureAlgorithm) -> anyhow::Result; + + async fn sign(self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result>; +} + +#[async_trait] +pub trait VerifyingKeystore { + async fn verify(self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> anyhow::Result<()>; +} + +#[async_trait] +pub trait ExportJwks { + async fn export_jwks(self) -> JsonWebKeySet; +} + +pub struct SharedSecret<'a> { + inner: &'a [u8], +} + +impl<'a> SharedSecret<'a> { + pub fn new(source: &'a impl AsRef<[u8]>) -> Self { + Self { + inner: source.as_ref(), + } + } +} + +#[async_trait] +impl<'a> SigningKeystore for &SharedSecret<'a> { + async fn prepare_header(self, alg: JsonWebSignatureAlgorithm) -> anyhow::Result { + if !matches!( + alg, + JsonWebSignatureAlgorithm::Hs256 + | JsonWebSignatureAlgorithm::Hs384 + | JsonWebSignatureAlgorithm::Hs512, + ) { + bail!("unsupported algorithm") + } + + Ok(JwtHeader::new(alg)) + } + + async fn sign(self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result> { + // TODO: do the signing in a blocking task + // TODO: should we bail out if the key is too small? + let signature = match header.alg() { + JsonWebSignatureAlgorithm::Hs256 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(msg); + mac.finalize().into_bytes().to_vec() + } + + JsonWebSignatureAlgorithm::Hs384 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(msg); + mac.finalize().into_bytes().to_vec() + } + + JsonWebSignatureAlgorithm::Hs512 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(msg); + mac.finalize().into_bytes().to_vec() + } + + _ => bail!("unsupported algorithm"), + }; + + Ok(signature) + } +} + +#[async_trait] +impl<'a> VerifyingKeystore for &SharedSecret<'a> { + async fn verify( + self, + header: &JwtHeader, + payload: &[u8], + signature: &[u8], + ) -> anyhow::Result<()> { + // TODO: do the verification in a blocking task + match header.alg() { + JsonWebSignatureAlgorithm::Hs256 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(payload); + mac.verify(signature)?; + } + + JsonWebSignatureAlgorithm::Hs384 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(payload); + mac.verify(signature)?; + } + + JsonWebSignatureAlgorithm::Hs512 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(payload); + mac.verify(signature)?; + } + + _ => bail!("unsupported algorithm"), + }; + + Ok(()) + } +} + +#[derive(Default)] +pub struct StaticKeystore { + rsa_keys: HashMap, + es256_keys: HashMap>, +} + +impl StaticKeystore { + #[must_use] + pub fn new() -> Self { + StaticKeystore::default() + } + + pub fn add_rsa_key(&mut self, key: rsa::RsaPrivateKey) -> anyhow::Result<()> { + let pubkey: &RsaPublicKey = &key; + let der = pubkey.to_pkcs1_der()?; + let digest = { + let mut digest = Sha256::new(); + digest.update(&der); + digest.finalize() + }; + // Truncate the digest to the 120 first bits + let digest = &digest[0..15]; + let digest = Base64UrlUnpadded::encode_string(digest); + let kid = format!("rsa-{}", digest); + self.rsa_keys.insert(kid, key); + Ok(()) + } + + pub fn add_ecdsa_key(&mut self, key: ecdsa::SigningKey) -> anyhow::Result<()> { + let pubkey: PublicKey = key.verifying_key().into(); + let der = EncodePublicKey::to_public_key_der(&pubkey)?; + let digest = { + let mut digest = Sha256::new(); + digest.update(&der); + digest.finalize() + }; + // Truncate the digest to the 120 first bits + let digest = &digest[0..15]; + let digest = Base64UrlUnpadded::encode_string(digest); + let kid = format!("ec-{}", digest); + self.es256_keys.insert(kid, key); + Ok(()) + } +} + +#[async_trait] +impl SigningKeystore for &StaticKeystore { + async fn prepare_header(self, alg: JsonWebSignatureAlgorithm) -> anyhow::Result { + let header = JwtHeader::new(alg); + + let kid = match alg { + JsonWebSignatureAlgorithm::Rs256 + | JsonWebSignatureAlgorithm::Rs384 + | JsonWebSignatureAlgorithm::Rs512 => self + .rsa_keys + .keys() + .next() + .ok_or_else(|| anyhow::anyhow!("no RSA keys in keystore"))?, + JsonWebSignatureAlgorithm::Es256 => self + .es256_keys + .keys() + .next() + .ok_or_else(|| anyhow::anyhow!("no ECDSA keys in keystore"))?, + _ => bail!("unsupported algorithm"), + }; + + Ok(header.with_kid(kid)) + } + + async fn sign(self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result> { + let kid = header + .kid() + .ok_or_else(|| anyhow::anyhow!("missing kid from the JWT header"))?; + + // TODO: do the signing in a blocking task + let signature = match header.alg() { + JsonWebSignatureAlgorithm::Rs256 => { + let key = self + .rsa_keys + .get(kid) + .ok_or_else(|| anyhow::anyhow!("RSA key not found in key store"))?; + + let digest = { + let mut digest = Sha256::new(); + digest.update(&msg); + digest.finalize() + }; + + key.sign( + rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_256)), + &digest, + )? + } + + JsonWebSignatureAlgorithm::Rs384 => { + let key = self + .rsa_keys + .get(kid) + .ok_or_else(|| anyhow::anyhow!("RSA key not found in key store"))?; + + let digest = { + let mut digest = Sha384::new(); + digest.update(&msg); + digest.finalize() + }; + + key.sign( + rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_384)), + &digest, + )? + } + + JsonWebSignatureAlgorithm::Rs512 => { + let key = self + .rsa_keys + .get(kid) + .ok_or_else(|| anyhow::anyhow!("RSA key not found in key store"))?; + + let digest = { + let mut digest = Sha512::new(); + digest.update(&msg); + digest.finalize() + }; + + key.sign( + rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_512)), + &digest, + )? + } + + JsonWebSignatureAlgorithm::Es256 => { + let key = self + .es256_keys + .get(kid) + .ok_or_else(|| anyhow::anyhow!("ECDSA key not found in key store"))?; + + let signature = key.try_sign(msg)?; + let signature: &[u8] = signature.as_ref(); + signature.to_vec() + } + + _ => bail!("Unsupported algorithm"), + }; + + Ok(signature) + } +} + +#[async_trait] +impl VerifyingKeystore for &StaticKeystore { + async fn verify( + self, + header: &JwtHeader, + payload: &[u8], + signature: &[u8], + ) -> anyhow::Result<()> { + let kid = header + .kid() + .ok_or_else(|| anyhow::anyhow!("missing kid claim in JWT header"))?; + + // TODO: do the verification in a blocking task + match header.alg() { + JsonWebSignatureAlgorithm::Rs256 => { + let key = self + .rsa_keys + .get(kid) + .ok_or_else(|| anyhow::anyhow!("could not find RSA key in key store"))?; + + let pubkey = rsa::RsaPublicKey::from(key); + + let digest = { + let mut digest = Sha256::new(); + digest.update(&payload); + digest.finalize() + }; + + pubkey.verify( + rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_256)), + &digest, + signature, + )?; + } + + JsonWebSignatureAlgorithm::Rs384 => { + let key = self + .rsa_keys + .get(kid) + .ok_or_else(|| anyhow::anyhow!("could not find RSA key in key store"))?; + + let pubkey = rsa::RsaPublicKey::from(key); + + let digest = { + let mut digest = Sha384::new(); + digest.update(&payload); + digest.finalize() + }; + + pubkey.verify( + rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_384)), + &digest, + signature, + )?; + } + + JsonWebSignatureAlgorithm::Rs512 => { + let key = self + .rsa_keys + .get(kid) + .ok_or_else(|| anyhow::anyhow!("could not find RSA key in key store"))?; + + let pubkey = rsa::RsaPublicKey::from(key); + + let digest = { + let mut digest = Sha512::new(); + digest.update(&payload); + digest.finalize() + }; + + pubkey.verify( + rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_512)), + &digest, + signature, + )?; + } + + JsonWebSignatureAlgorithm::Es256 => { + let key = self + .es256_keys + .get(kid) + .ok_or_else(|| anyhow::anyhow!("could not find ECDSA key in key store"))?; + + let pubkey = VerifyingKey::from(key); + let signature = ecdsa::Signature::from_bytes(signature)?; + + pubkey.verify(payload, &signature)?; + } + _ => bail!("unsupported algorithm"), + } + + Ok(()) + } +} + +#[async_trait] +impl ExportJwks for &StaticKeystore { + async fn export_jwks(self) -> JsonWebKeySet { + let rsa = self.rsa_keys.iter().flat_map(|(kid, key)| { + let pubkey = RsaPublicKey::from(key); + let basekey = JsonWebKey::new(pubkey.into()) + .with_kid(kid) + .with_use(crate::JsonWebKeyUse::Sig) + .with_key_ops(vec![JsonWebKeyOperation::Sign]); + + let algs = [ + JsonWebSignatureAlgorithm::Rs256, + JsonWebSignatureAlgorithm::Rs384, + JsonWebSignatureAlgorithm::Rs512, + ]; + + algs.into_iter() + .map(move |alg| basekey.clone().with_alg(alg)) + }); + + let es256 = self.es256_keys.iter().map(|(kid, key)| { + let pubkey = ecdsa::VerifyingKey::from(key); + JsonWebKey::new(pubkey.into()) + .with_kid(kid) + .with_use(crate::JsonWebKeyUse::Sig) + .with_key_ops(vec![JsonWebKeyOperation::Sign]) + .with_alg(JsonWebSignatureAlgorithm::Es256) + }); + + let keys = rsa.chain(es256).collect(); + JsonWebKeySet::new(keys) + } +} + +#[cfg(test)] +mod tests { + use ecdsa::SigningKey; + use pkcs1::DecodeRsaPrivateKey; + use pkcs8::DecodePrivateKey; + use rsa::RsaPrivateKey; + + use super::*; + + // Generate with + // openssl genrsa 2048 + const RSA_PKCS1_PEM: &str = "-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1j7Y2CH6Ss8tgaNvcQPaRJKnCZD8ABqNPyKDWLQLph6Zi7gZ +GqmRtTzMuevo2ezpkbCiQAPEp1ms022P92bB+uqG7xmzHTzbwLtnq3OAdjmrnaFV +I4v89WHUsTXX9hiYOK5dOM81bNZ6muxWZ0L/xw4jVWe7xkqnp2Lluq0HknlzP5yJ +UEikf5BkpX0iyIu2/X4r8YVp8uzG34l/8qBx6k3rO2VkOQOSybZj1oij5KZCusnu +QjJLKWXCqJToWE6iVn+Q0N6ySDLgmJ7Zq0Sou/9N/oWKn94FOsouQgET5NuzoIFR +qTb321fQ8gbqt/OupBbBKEo1qUU+cS77TD/AuQIDAQABAoIBAQDLSZzmD+93lnf+ +f36ZxOcRk/nNGPYUfx0xH+VzgHthJ73YFlozs1xflQ5JB/DM/4BsziZWCX1KsctM +XrRxMt6y4GAidcc/4eQ+T1RCGfl1tKkDi/bGIOloSGjRsV5208V0WvZ3lh2CZUy2 +vbQKjUc3sFGUkzZYI7RLHosPA2mg78IVuSnqvNaU0TgA2KkaxWs6Ecr/ys80cUvj +KKj04DmX5xaXwUKmz353i5gIt3aY3G5CAw5fU/ocDKR8nzVCpBAGbRRiUaVKIT06 +APSkLDTUnxSYtHtDJGHjgU/TsvAwTA92J3ue5Ysu9xTE+WyHA6Rgux7RQSD/wWHr +LdRPwxPFAoGBAOytMPh/f2zKmotanjho0QNfhAUHoQUfPudYT0nnDceOsi1jYWbQ +c/wPeQQC4Hp/pTUrkSIQPEz/hSxzZ6RPxxuGB8O94I0uLwQK4V1UwbgfsRa9zQzW +n0kgKZ8w8h8B7qyiKyIAnZzvKtNEnKrzrct4HsN3OEoXTwuAUYlvWtQTAoGBAOe8 +0liNaH9V6ecZiojkRR1tiQkr/dCV+a13eXXaRA/8y/3wKCQ4idYncclQJTLKsAwW +hHuDd4uLgtifREVIBD2jGdlznNr9HQNuZgwjuUoH+r1YLGgiMWeVYSr0m8lyDlQl +BJKTAphrqo6VJWDAnM18v+by//yRleSjVMqZ3zmDAoGBAMpA0rl5EyagGON/g/hG +sl8Ej+hQdazP38yJbfCEsATaD6+z3rei6Yr8mfjwkG5+iGrgmT0XzMAsF909ndMP +jeIabqY6rBtZ3TnCJobAeG9lPctmVUVkX2h5QLhWdoJC/3iteNis2AQVam5yksOQ +S/O16ew2BHdkZds5Q/SDoYXbAoGAK9tVZ8LjWu30hXMU/9FLr0USoTS9JWOszAKH +byFuriPmq1lvD2PP2kK+yx2q3JD1fmQokIOR9Uvi6IJD1mTJwKyEcN3reppailKz +Z2q/X15hOsJcLR0DgpoHuKxwa1B1m8Ehu2etHxGJRtC9MTFiu5T3cIrenXskBhBP +NMSoNWcCgYAD3u3zdeVo3gVoxneS7GNVI2WBhjtqgNIbINuxGZvfztm7+vNPE6sQ +VL8i+09uoM1H6sXbe2XXORmtW0j/6MmYhSoBXNdqWTNAiyNRhwEQtowqgl5R7PBu +//QZTF1z62R9IKDMRG3f5Wn8e1Dys6tXBuG603g+Dkkc/km476mrgw== +-----END RSA PRIVATE KEY-----"; + + // Generate with + // openssl ecparam -genkey -name prime256v1 | openssl pkcs8 -topk8 -nocrypt + const EC_PKCS8_PEM: &str = "-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+tHxet7G+uar2Cef +iYPb7jv3uzncFtwJ7RhDOvEA0fChRANCAATCKn2AEqa9785k+TmwkeCvLub8XGrF +ezE6bA/blaPVE3nu4SUVYKULRJQxNjeOSra8TQrlIS8e5ItbMn8Tv9KV +-----END PRIVATE KEY-----"; + + #[tokio::test] + async fn test_shared_secret() { + let secret = "super-complicated-secret-that-should-be-big-enough-for-sha512"; + let message = "this is the message to sign".as_bytes(); + let store = SharedSecret::new(&secret); + for alg in [ + JsonWebSignatureAlgorithm::Hs256, + JsonWebSignatureAlgorithm::Hs384, + JsonWebSignatureAlgorithm::Hs512, + ] { + let header = store.prepare_header(alg).await.unwrap(); + assert_eq!(header.alg(), alg); + let signature = store.sign(&header, message).await.unwrap(); + store.verify(&header, message, &signature).await.unwrap(); + } + } + + #[tokio::test] + async fn test_static_store() { + let message = "this is the message to sign".as_bytes(); + let store = { + let mut s = StaticKeystore::new(); + + let rsa = RsaPrivateKey::from_pkcs1_pem(RSA_PKCS1_PEM).unwrap(); + s.add_rsa_key(rsa).unwrap(); + + let ecdsa = SigningKey::from_pkcs8_pem(EC_PKCS8_PEM).unwrap(); + s.add_ecdsa_key(ecdsa).unwrap(); + + s + }; + + for alg in [ + JsonWebSignatureAlgorithm::Rs256, + JsonWebSignatureAlgorithm::Rs384, + JsonWebSignatureAlgorithm::Rs512, + JsonWebSignatureAlgorithm::Es256, + ] { + let header = store.prepare_header(alg).await.unwrap(); + assert_eq!(header.alg(), alg); + let signature = store.sign(&header, message).await.unwrap(); + store.verify(&header, message, &signature).await.unwrap(); + } + } +} diff --git a/crates/jose/src/lib.rs b/crates/jose/src/lib.rs new file mode 100644 index 00000000..f55ff85e --- /dev/null +++ b/crates/jose/src/lib.rs @@ -0,0 +1,35 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![forbid(unsafe_code)] +#![deny(clippy::all)] +#![deny(rustdoc::broken_intra_doc_links)] +#![warn(clippy::pedantic)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::module_name_repetitions)] + +pub(crate) mod iana; +pub(crate) mod jwk; +pub(crate) mod jwt; +mod keystore; + +pub use self::{ + iana::{ + JsonWebEncryptionAlgorithm, JsonWebEncryptionCompressionAlgorithm, JsonWebKeyOperation, + JsonWebKeyType, JsonWebKeyUse, JsonWebSignatureAlgorithm, + }, + jwk::{JsonWebKey, JsonWebKeySet}, + jwt::{DecodedJsonWebToken, JsonWebTokenParts, JwtHeader}, + keystore::{ExportJwks, SharedSecret, SigningKeystore, StaticKeystore, VerifyingKeystore}, +}; diff --git a/crates/storage/src/user.rs b/crates/storage/src/user.rs index f26ebb43..46297903 100644 --- a/crates/storage/src/user.rs +++ b/crates/storage/src/user.rs @@ -159,6 +159,7 @@ impl TryInto> for SessionLookup { } } +#[tracing::instrument(skip_all, fields(session.id = id))] pub async fn lookup_active_session( executor: impl PgExecutor<'_>, id: i64, @@ -191,6 +192,7 @@ pub async fn lookup_active_session( Ok(res) } +#[tracing::instrument(skip_all, fields(user.id = user.data))] pub async fn start_session( executor: impl PgExecutor<'_>, user: User, diff --git a/crates/warp-utils/Cargo.toml b/crates/warp-utils/Cargo.toml index 8a9bd2b0..9741c281 100644 --- a/crates/warp-utils/Cargo.toml +++ b/crates/warp-utils/Cargo.toml @@ -14,7 +14,6 @@ hyper = { version = "0.14.16", features = ["full"] } thiserror = "1.0.30" anyhow = "1.0.51" sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] } -jwt-compact = { version = "0.5.0-beta.1", features = ["with_rsa", "k256"] } chrono = { version = "0.4.19", features = ["serde"] } serde = { version = "1.0.131", features = ["derive"] } serde_with = { version = "1.11.0", features = ["hex", "chrono"] } @@ -35,3 +34,4 @@ mas-config = { path = "../config" } mas-templates = { path = "../templates" } mas-data-model = { path = "../data-model" } mas-storage = { path = "../storage" } +mas-jose = { path = "../jose" } diff --git a/crates/warp-utils/src/filters/client.rs b/crates/warp-utils/src/filters/client.rs index 25140bb6..fda58a65 100644 --- a/crates/warp-utils/src/filters/client.rs +++ b/crates/warp-utils/src/filters/client.rs @@ -14,15 +14,9 @@ //! Handle client authentication -use std::borrow::Cow; - -use chrono::{Duration, Utc}; use headers::{authorization::Basic, Authorization}; -use jwt_compact::{ - alg::{Hs256, Hs256Key, Hs384, Hs384Key, Hs512, Hs512Key}, - Algorithm, AlgorithmExt, AlgorithmSignature, TimeOptions, Token, UntrustedToken, -}; use mas_config::{OAuth2ClientConfig, OAuth2Config}; +use mas_jose::{DecodedJsonWebToken, JsonWebTokenParts, SharedSecret}; use oauth2_types::requests::ClientAuthenticationMethod; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -113,67 +107,6 @@ struct ClientAssertionClaims { jwt_id: Option, } -struct UnsignedSignature(Vec); -impl AlgorithmSignature for UnsignedSignature { - fn try_from_slice(slice: &[u8]) -> anyhow::Result { - Ok(Self(slice.to_vec())) - } - - fn as_bytes(&self) -> std::borrow::Cow<'_, [u8]> { - Cow::Borrowed(&self.0) - } -} - -struct Unsigned<'a>(&'a str); -impl<'a> Algorithm for Unsigned<'a> { - type SigningKey = (); - - type VerifyingKey = (); - - type Signature = UnsignedSignature; - - fn name(&self) -> std::borrow::Cow<'static, str> { - Cow::Owned(self.0.to_string()) - } - - fn sign(&self, _signing_key: &Self::SigningKey, _message: &[u8]) -> Self::Signature { - UnsignedSignature(Vec::new()) - } - - fn verify_signature( - &self, - _signature: &Self::Signature, - _verifying_key: &Self::VerifyingKey, - _message: &[u8], - ) -> bool { - true - } -} - -fn verify_token( - untrusted_token: &UntrustedToken, - key: &str, -) -> anyhow::Result> { - match untrusted_token.algorithm() { - "HS256" => { - let key = Hs256Key::new(key); - let token = Hs256.validate_integrity(untrusted_token, &key)?; - Ok(token) - } - "HS384" => { - let key = Hs384Key::new(key); - let token = Hs384.validate_integrity(untrusted_token, &key)?; - Ok(token) - } - "HS512" => { - let key = Hs512Key::new(key); - let token = Hs512.validate_integrity(untrusted_token, &key)?; - Ok(token) - } - alg => anyhow::bail!("unsupported signing algorithm {}", alg), - } -} - async fn authenticate_client( clients: Vec, audience: String, @@ -211,23 +144,13 @@ async fn authenticate_client( client_assertion_type: ClientAssertionType::JwtBearer, client_assertion, } => { - let untrusted_token = UntrustedToken::new(&client_assertion).wrap_error()?; + let token: JsonWebTokenParts = client_assertion.parse().wrap_error()?; + let decoded: DecodedJsonWebToken = + token.decode().wrap_error()?; // client_id might have been passed as parameter. If not, it should be inferred // from the token, as per rfc7521 sec. 4.2 - // TODO: this is not a pretty way to do it - let client_id = client_id - .ok_or(()) // Dumb error type - .or_else(|()| { - let alg = Unsigned(untrusted_token.algorithm()); - // We need to deserialize the token once without verifying the signature to get - // the client_id - let token: Token = - alg.validate_integrity(&untrusted_token, &())?; - - Ok::<_, anyhow::Error>(token.claims().custom.subject.clone()) - }) - .wrap_error()?; + let client_id = client_id.unwrap_or_else(|| decoded.claims().subject.clone()); let client = clients .iter() @@ -237,32 +160,20 @@ async fn authenticate_client( })?; if let Some(client_secret) = &client.client_secret { - let token = verify_token(&untrusted_token, client_secret).wrap_error()?; - - let time_options = TimeOptions::new(Duration::minutes(1), Utc::now); - - // rfc7523 sec. 3.4: expiration must be set and validated - let claims = token - .claims() - .validate_expiration(&time_options) - .wrap_error()?; - - // rfc7523 sec. 3.5: "not before" can be set and must be validated if present - if claims.not_before.is_some() { - claims.validate_maturity(&time_options).wrap_error()?; - } + let store = SharedSecret::new(client_secret); + token.verify(&decoded, &store).await.wrap_error()?; + let claims = decoded.claims(); + // TODO: validate the times again // rfc7523 sec. 3.3: the audience is the URL being called - if claims.custom.audience != audience { + if claims.audience != audience { Err(ClientAuthenticationError::AudienceMismatch { expected: audience, - got: claims.custom.audience.clone(), + got: claims.audience.clone(), }) // rfc7523 sec. 3.1 & 3.2: both the issuer and the subject must // match the client_id - } else if claims.custom.issuer != claims.custom.subject - || claims.custom.issuer != client_id - { + } else if claims.issuer != claims.subject || claims.issuer != client_id { Err(ClientAuthenticationError::InvalidAssertion) } else { Ok(client) @@ -348,8 +259,8 @@ struct ClientAuthForm { #[cfg(test)] mod tests { use headers::authorization::Credentials; - use jwt_compact::{Claims, Header}; use mas_config::ConfigurationSection; + use mas_jose::{JsonWebSignatureAlgorithm, SigningKeystore}; use serde_json::json; use super::*; @@ -385,38 +296,34 @@ mod tests { #[tokio::test] async fn client_secret_jwt_hs256() { - client_secret_jwt::<'_, Hs256>().await; + client_secret_jwt(JsonWebSignatureAlgorithm::Hs256).await; } #[tokio::test] async fn client_secret_jwt_hs384() { - client_secret_jwt::<'_, Hs384>().await; + client_secret_jwt(JsonWebSignatureAlgorithm::Hs384).await; } #[tokio::test] async fn client_secret_jwt_hs512() { - client_secret_jwt::<'_, Hs512>().await; + client_secret_jwt(JsonWebSignatureAlgorithm::Hs512).await; } - async fn client_secret_jwt<'k, A>() - where - A: Algorithm + Default, - A::SigningKey: From<&'k [u8]>, - { + async fn client_secret_jwt(alg: JsonWebSignatureAlgorithm) { let audience = "https://example.com/token".to_string(); let filter = client_authentication::
(&oauth2_config(), audience.clone()); - let time_options = TimeOptions::default(); - let key = A::SigningKey::from(CLIENT_SECRET.as_bytes()); - let alg = A::default(); - let header = Header::default(); - let claims = Claims::new(ClientAssertionClaims { + let store = SharedSecret::new(&CLIENT_SECRET); + let claims = ClientAssertionClaims { issuer: "confidential".to_string(), subject: "confidential".to_string(), audience, jwt_id: None, - }) - .set_duration_and_issuance(&time_options, Duration::seconds(15)); + }; + let header = store.prepare_header(alg).await.expect("JWT header"); + let jwt = DecodedJsonWebToken::new(header, claims); + let jwt = jwt.sign(&store).await.expect("signed token"); + let jwt = jwt.serialize(); // TODO: test failing cases // - expired token @@ -425,16 +332,12 @@ mod tests { // - audience mismatch // - wrong secret/signature - let token = alg - .token(header, &claims, &key) - .expect("could not sign token"); - let (auth, client, body) = warp::test::request() .method("POST") .header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string()) .body(serde_urlencoded::to_string(json!({ "client_id": "confidential", - "client_assertion": token, + "client_assertion": jwt, "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "foo": "baz", "bar": "foobar", @@ -453,7 +356,7 @@ mod tests { .method("POST") .header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string()) .body(serde_urlencoded::to_string(json!({ - "client_assertion": token, + "client_assertion": jwt, "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "foo": "baz", "bar": "foobar", @@ -467,7 +370,7 @@ mod tests { .method("POST") .body(serde_urlencoded::to_string(json!({ "client_id": "confidential-2", - "client_assertion": token, + "client_assertion": jwt, "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "foo": "baz", "bar": "foobar", diff --git a/crates/warp-utils/src/filters/mod.rs b/crates/warp-utils/src/filters/mod.rs index 160999e9..ed9f29af 100644 --- a/crates/warp-utils/src/filters/mod.rs +++ b/crates/warp-utils/src/filters/mod.rs @@ -28,7 +28,6 @@ pub mod session; use std::convert::Infallible; -use mas_config::{KeySet, OAuth2Config}; use mas_templates::Templates; use warp::{Filter, Rejection}; @@ -43,15 +42,6 @@ pub fn with_templates( warp::any().map(move || templates.clone()) } -/// Extract the [`KeySet`] from the [`OAuth2Config`] -#[must_use] -pub fn with_keys( - oauth2_config: &OAuth2Config, -) -> impl Filter + Clone + Send + Sync + 'static { - let keyset = oauth2_config.keys.clone(); - warp::any().map(move || keyset.clone()) -} - /// Recover a particular rejection type with a `None` option variant /// /// # Example