diff --git a/Cargo.lock b/Cargo.lock index 51d4ce7db..b35248c6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "serde", "version_check", @@ -232,9 +232,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "flate2", "futures-core", @@ -621,9 +621,9 @@ checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" [[package]] name = "bstr" -version = "1.11.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata 0.4.9", @@ -668,9 +668,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.17" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] @@ -792,8 +792,7 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_jwt" version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bbab6445446e8d0b07468a01d0bfdae15879de5c440c5e47ae4ae0e18a1fba" +source = "git+https://github.com/Firstyear/compact-jwt.git?rev=b3d2b5700cfe567d384c81df35d25537fbf7f110#b3d2b5700cfe567d384c81df35d25537fbf7f110" dependencies = [ "base64 0.21.7", "base64urlsafedata", @@ -1053,9 +1052,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -1063,9 +1062,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", @@ -1077,9 +1076,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", @@ -1088,15 +1087,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "der_derive", @@ -1132,9 +1131,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", "serde", @@ -1223,22 +1222,23 @@ dependencies = [ [[package]] name = "dirs" -version = "4.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.7" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -1351,9 +1351,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1361,9 +1361,9 @@ dependencies = [ [[package]] name = "escargot" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05a3ac187a16b5382fef8c69fd1bad123c67b7cf3932240a2d43dcdd32cded88" +checksum = "83f351750780493fc33fa0ce8ba3c7d61f9736cfa3b3bb9ee2342643ffe40211" dependencies = [ "log", "once_cell", @@ -1444,19 +1444,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fernet" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66b725fe9483b9ee72ccaec072b15eb8ad95a3ae63a8c798d5748883b72fd33" -dependencies = [ - "base64 0.22.1", - "byteorder", - "getrandom 0.2.15", - "openssl", - "zeroize", -] - [[package]] name = "file-id" version = "0.2.2" @@ -1486,15 +1473,15 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flagset" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -1573,7 +1560,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" dependencies = [ - "rustix 1.0.3", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -1687,9 +1674,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", @@ -2277,7 +2264,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.8.0", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -2286,9 +2273,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" dependencies = [ "atomic-waker", "bytes", @@ -2296,7 +2283,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.8.0", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -2503,7 +2490,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.8", + "h2 0.4.9", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -2565,9 +2552,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -2575,6 +2562,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", + "libc", "pin-project-lite", "socket2", "tokio", @@ -2584,9 +2572,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2801,9 +2789,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3277,7 +3265,6 @@ dependencies = [ "concread", "dhat", "dyn-clone", - "fernet", "futures", "hashbrown 0.15.2", "hex", @@ -3473,9 +3460,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" [[package]] name = "libmimalloc-sys" @@ -3547,9 +3534,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -3732,18 +3719,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mintex" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bec4598fddb13cc7b528819e697852653252b760f1228b7642679bf2ff2cd07" +checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" [[package]] name = "mio" @@ -4034,7 +4021,7 @@ checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" dependencies = [ "base64 0.13.1", "chrono", - "getrandom 0.2.15", + "getrandom 0.2.16", "http 0.2.12", "rand 0.8.5", "reqwest 0.11.27", @@ -4054,7 +4041,7 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ "base64 0.22.1", "chrono", - "getrandom 0.2.15", + "getrandom 0.2.16", "http 1.3.1", "rand 0.8.5", "reqwest 0.12.15", @@ -4095,9 +4082,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" @@ -4228,6 +4215,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orca" version = "1.6.0-dev" @@ -4372,7 +4365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.8.0", + "indexmap 2.9.0", "serde", ] @@ -4491,9 +4484,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", "syn 2.0.100", @@ -4501,12 +4494,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "once_cell", - "toml_edit 0.19.15", + "toml_edit", ] [[package]] @@ -4628,9 +4620,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" dependencies = [ "bytes", "getrandom 0.3.2", @@ -4722,7 +4714,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -4742,22 +4734,22 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags 2.9.0", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -4888,7 +4880,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.8", + "h2 0.4.9", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -4942,7 +4934,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -4980,9 +4972,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4991,9 +4983,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e" dependencies = [ "proc-macro2", "quote", @@ -5004,9 +4996,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21" dependencies = [ "sha2", "walkdir", @@ -5054,22 +5046,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys 0.9.3", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "once_cell", "ring", @@ -5352,7 +5344,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.8.0", + "indexmap 2.9.0", "serde", "serde_derive", "serde_json", @@ -5448,9 +5440,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -5483,9 +5475,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" dependencies = [ "serde", ] @@ -5513,9 +5505,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -5686,7 +5678,7 @@ dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", - "rustix 1.0.3", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -5917,9 +5909,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -5937,7 +5929,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.24", + "toml_edit", ] [[package]] @@ -5949,24 +5941,13 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.8.0", - "toml_datetime", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", @@ -5984,7 +5965,7 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "h2 0.4.8", + "h2 0.4.9", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -6309,7 +6290,7 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.9.0", "serde", "serde_json", "utoipa-gen", @@ -6729,11 +6710,37 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] @@ -6749,7 +6756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", - "windows-strings", + "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -6771,6 +6778,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -7025,15 +7041,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.6.26" diff --git a/Cargo.toml b/Cargo.toml index 6d614450e..0b91a387b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,7 @@ codegen-units = 256 ## As Kanidm maintains a number of libraries, sometimes during development we need to override them ## with local or git versions. This patch table allows quick uncommenting to achieve that. -# compact_jwt = { path = "../compact_jwt" } +# compact_jwt = { path = "../compact-jwt" } # concread = { path = "../concread" } # idlset = { path = "../idlset" } @@ -122,7 +122,10 @@ codegen-units = 256 libnss = { git = "https://github.com/Firstyear/libnss-rs.git", branch = "20250207-freebsd" } # Allow ssh keys to have comments with spaces. sshkeys = { git = "https://github.com/Firstyear/rust-sshkeys.git", rev = "3a081cbf7480628223bcb96fc8aaa8c19109d007" } - +# Branch currently carrying some needed rs256 signer patches for jwk handling, +# as main is currently working to drop openssl and may need more work before +# we commit to that change here. +compact_jwt = { git = "https://github.com/Firstyear/compact-jwt.git", rev = "b3d2b5700cfe567d384c81df35d25537fbf7f110" } [workspace.dependencies] kanidmd_core = { path = "./server/core", version = "=1.6.0-dev" } @@ -173,7 +176,6 @@ csv = "1.3.1" dialoguer = "0.11.0" dhat = "0.3.3" dyn-clone = "^1.0.17" -fernet = "^0.2.1" filetime = "^0.2.24" fs4 = "^0.13.0" futures = "^0.3.31" diff --git a/libs/client/src/oauth.rs b/libs/client/src/oauth.rs index 9b4ce756d..526268ec0 100644 --- a/libs/client/src/oauth.rs +++ b/libs/client/src/oauth.rs @@ -1,16 +1,18 @@ use crate::{ClientError, KanidmClient}; use kanidm_proto::attribute::Attribute; use kanidm_proto::constants::{ - ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_NAME, + ATTR_DISPLAYNAME, ATTR_KEY_ACTION_REVOKE, ATTR_KEY_ACTION_ROTATE, ATTR_NAME, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT, ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_PREFER_SHORT_USERNAME, ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_ORIGIN, ATTR_OAUTH2_RS_ORIGIN_LANDING, - ATTR_OAUTH2_RS_TOKEN_KEY, ATTR_OAUTH2_STRICT_REDIRECT_URI, ATTR_RS256_PRIVATE_KEY_DER, + ATTR_OAUTH2_STRICT_REDIRECT_URI, }; use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin}; use kanidm_proto::v1::Entry; use reqwest::multipart; use std::collections::BTreeMap; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; use url::Url; impl KanidmClient { @@ -84,7 +86,34 @@ impl KanidmClient { .await } - #[allow(clippy::too_many_arguments)] + pub async fn idm_oauth2_rs_revoke_key( + &self, + id: &str, + key_id: &str, + ) -> Result<(), ClientError> { + self.perform_post_request( + &format!("/v1/oauth2/{}/_attr/{}", id, ATTR_KEY_ACTION_REVOKE), + vec![key_id.to_string()], + ) + .await + } + + pub async fn idm_oauth2_rs_rotate_keys( + &self, + id: &str, + rotate_at_time: OffsetDateTime, + ) -> Result<(), ClientError> { + let rfc_3339_str = rotate_at_time.format(&Rfc3339).map_err(|_| { + ClientError::InvalidRequest("Unable to format rfc 3339 datetime".into()) + })?; + + self.perform_post_request( + &format!("/v1/oauth2/{}/_attr/{}", id, ATTR_KEY_ACTION_ROTATE), + vec![rfc_3339_str], + ) + .await + } + pub async fn idm_oauth2_rs_update( &self, id: &str, @@ -92,8 +121,6 @@ impl KanidmClient { displayname: Option<&str>, landing: Option<&str>, reset_secret: bool, - reset_token_key: bool, - reset_sign_key: bool, ) -> Result<(), ClientError> { let mut update_oauth2_rs = Entry { attrs: BTreeMap::new(), @@ -121,19 +148,6 @@ impl KanidmClient { .attrs .insert(ATTR_OAUTH2_RS_BASIC_SECRET.to_string(), Vec::new()); } - if reset_token_key { - update_oauth2_rs - .attrs - .insert(ATTR_OAUTH2_RS_TOKEN_KEY.to_string(), Vec::new()); - } - if reset_sign_key { - update_oauth2_rs - .attrs - .insert(ATTR_ES256_PRIVATE_KEY_DER.to_string(), Vec::new()); - update_oauth2_rs - .attrs - .insert(ATTR_RS256_PRIVATE_KEY_DER.to_string(), Vec::new()); - } self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs) .await } diff --git a/proto/src/attribute.rs b/proto/src/attribute.rs index 493509944..174942689 100644 --- a/proto/src/attribute.rs +++ b/proto/src/attribute.rs @@ -89,6 +89,7 @@ pub enum Attribute { KeyActionRotate, KeyActionRevoke, KeyActionImportJwsEs256, + KeyActionImportJwsRs256, KeyInternalData, KeyProvider, LastModifiedCid, @@ -323,6 +324,7 @@ impl Attribute { Attribute::KeyActionRotate => ATTR_KEY_ACTION_ROTATE, Attribute::KeyActionRevoke => ATTR_KEY_ACTION_REVOKE, Attribute::KeyActionImportJwsEs256 => ATTR_KEY_ACTION_IMPORT_JWS_ES256, + Attribute::KeyActionImportJwsRs256 => ATTR_KEY_ACTION_IMPORT_JWS_RS256, Attribute::KeyInternalData => ATTR_KEY_INTERNAL_DATA, Attribute::KeyProvider => ATTR_KEY_PROVIDER, Attribute::LastModifiedCid => ATTR_LAST_MODIFIED_CID, @@ -510,6 +512,7 @@ impl Attribute { ATTR_KEY_ACTION_ROTATE => Attribute::KeyActionRotate, ATTR_KEY_ACTION_REVOKE => Attribute::KeyActionRevoke, ATTR_KEY_ACTION_IMPORT_JWS_ES256 => Attribute::KeyActionImportJwsEs256, + ATTR_KEY_ACTION_IMPORT_JWS_RS256 => Attribute::KeyActionImportJwsRs256, ATTR_KEY_INTERNAL_DATA => Attribute::KeyInternalData, ATTR_KEY_PROVIDER => Attribute::KeyProvider, ATTR_LAST_MODIFIED_CID => Attribute::LastModifiedCid, diff --git a/proto/src/constants.rs b/proto/src/constants.rs index 414c51791..aaa1c7392 100644 --- a/proto/src/constants.rs +++ b/proto/src/constants.rs @@ -133,6 +133,7 @@ pub const ATTR_JWS_ES256_PRIVATE_KEY: &str = "jws_es256_private_key"; pub const ATTR_KEY_ACTION_ROTATE: &str = "key_action_rotate"; pub const ATTR_KEY_ACTION_REVOKE: &str = "key_action_revoke"; pub const ATTR_KEY_ACTION_IMPORT_JWS_ES256: &str = "key_action_import_jws_es256"; +pub const ATTR_KEY_ACTION_IMPORT_JWS_RS256: &str = "key_action_import_jws_rs256"; pub const ATTR_KEY_INTERNAL_DATA: &str = "key_internal_data"; pub const ATTR_KEY_PROVIDER: &str = "key_provider"; pub const ATTR_LAST_MODIFIED_CID: &str = "last_modified_cid"; @@ -319,5 +320,6 @@ pub const ENTRYCLASS_KEY_PROVIDER: &str = "key_provider"; pub const ENTRYCLASS_KEY_PROVIDER_INTERNAL: &str = "key_provider_internal"; pub const ENTRYCLASS_KEY_OBJECT: &str = "key_object"; pub const ENTRYCLASS_KEY_OBJECT_JWT_ES256: &str = "key_object_jwt_es256"; +pub const ENTRYCLASS_KEY_OBJECT_JWT_RS256: &str = "key_object_jwt_rs256"; pub const ENTRYCLASS_KEY_OBJECT_JWE_A128GCM: &str = "key_object_jwe_a128gcm"; pub const ENTRYCLASS_KEY_OBJECT_INTERNAL: &str = "key_object_internal"; diff --git a/proto/src/internal/error.rs b/proto/src/internal/error.rs index 09f6cb144..3facaa936 100644 --- a/proto/src/internal/error.rs +++ b/proto/src/internal/error.rs @@ -270,6 +270,25 @@ pub enum OperationError { KP0043KeyObjectJweA128GCMEncryption, KP0044KeyObjectJwsPublicJwk, + KP0045KeyObjectImportJwsRs256DerInvalid, + KP0046KeyObjectSignerToVerifier, + KP0047KeyObjectPublicToDer, + KP0048KeyObjectJwtRs256Generation, + KP0049KeyObjectSignerToVerifier, + KP0050KeyObjectPrivateToDer, + KP0051KeyObjectPublicToDer, + KP0052KeyObjectJwsRs256DerInvalid, + KP0053KeyObjectSignerToVerifier, + KP0054KeyObjectJwsRs256DerInvalid, + KP0055KeyObjectJwsRs256DerInvalid, + KP0056KeyObjectJwsRs256Signature, + KP0057KeyObjectJwsNotAssociated, + KP0058KeyObjectJwsInvalid, + KP0059KeyObjectJwsKeyRevoked, + KP0060KeyObjectJwsPublicJwk, + KP0061KeyObjectNoActiveSigningKeys, + KP0062KeyProviderNoSuchKey, + // Plugins PL0001GidOverlapsSystemRange, @@ -448,6 +467,26 @@ impl OperationError { Self::KP0042KeyObjectNoActiveEncryptionKeys => None, Self::KP0043KeyObjectJweA128GCMEncryption => None, Self::KP0044KeyObjectJwsPublicJwk => None, + + Self::KP0045KeyObjectImportJwsRs256DerInvalid => None, + Self::KP0046KeyObjectSignerToVerifier => None, + Self::KP0047KeyObjectPublicToDer => None, + Self::KP0048KeyObjectJwtRs256Generation => None, + Self::KP0049KeyObjectSignerToVerifier => None, + Self::KP0050KeyObjectPrivateToDer => None, + Self::KP0051KeyObjectPublicToDer => None, + Self::KP0052KeyObjectJwsRs256DerInvalid => None, + Self::KP0053KeyObjectSignerToVerifier => None, + Self::KP0054KeyObjectJwsRs256DerInvalid => None, + Self::KP0055KeyObjectJwsRs256DerInvalid => None, + Self::KP0056KeyObjectJwsRs256Signature => None, + Self::KP0057KeyObjectJwsNotAssociated => None, + Self::KP0058KeyObjectJwsInvalid => None, + Self::KP0059KeyObjectJwsKeyRevoked => None, + Self::KP0060KeyObjectJwsPublicJwk => None, + Self::KP0061KeyObjectNoActiveSigningKeys => None, + Self::KP0062KeyProviderNoSuchKey => None, + Self::KU001InitWhileSessionActive => Some("The session was active when the init function was called.".into()), Self::KU002ContinueWhileSessionInActive => Some("Attempted to continue auth session while current session is inactive".into()), Self::KU003PamAuthFailed => Some("Failed PAM account authentication step".into()), diff --git a/server/Dockerfile b/server/Dockerfile index 5c92524c1..1bfc8586a 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -65,6 +65,8 @@ RUN <<EOF ls -Rla /out/libs-root EOF +RUN ls /usr/src/kanidm/target/release/kanidmd + # ====================== FROM scratch diff --git a/server/daemon/insecure_server.toml b/server/daemon/insecure_server.toml index b0cc23bc8..660ae9cf2 100644 --- a/server/daemon/insecure_server.toml +++ b/server/daemon/insecure_server.toml @@ -13,9 +13,9 @@ tls_key = "/tmp/kanidm/key.pem" # NOTE: this is overridden by KANIDM_LOG_LEVEL environment variable # Defaults to "info" # -log_level = "info" +# log_level = "info" # log_level = "debug" -# log_level = "trace" +log_level = "trace" # otel_grpc_url = "http://localhost:4317" diff --git a/server/lib/Cargo.toml b/server/lib/Cargo.toml index aad01fb19..7b048e5d3 100644 --- a/server/lib/Cargo.toml +++ b/server/lib/Cargo.toml @@ -32,7 +32,6 @@ compact_jwt = { workspace = true, features = ["openssl", "hsm-crypto"] } concread = { workspace = true } dhat = { workspace = true, optional = true } dyn-clone = { workspace = true } -fernet = { workspace = true, features = ["fernet_danger_timestamps"] } # futures-util = { workspace = true } hashbrown = { workspace = true } idlset = { workspace = true } diff --git a/server/lib/src/be/dbvalue.rs b/server/lib/src/be/dbvalue.rs index b6fbc5df0..dc0d5d040 100644 --- a/server/lib/src/be/dbvalue.rs +++ b/server/lib/src/be/dbvalue.rs @@ -692,6 +692,7 @@ pub enum DbValueImage { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum DbValueKeyUsage { JwsEs256, + JwsRs256, JweA128GCM, } diff --git a/server/lib/src/constants/entries.rs b/server/lib/src/constants/entries.rs index 36f48aac4..e8f0f1b56 100644 --- a/server/lib/src/constants/entries.rs +++ b/server/lib/src/constants/entries.rs @@ -40,6 +40,7 @@ pub enum EntryClass { KeyProviderInternal, KeyObject, KeyObjectJwtEs256, + KeyObjectJwtRs256, KeyObjectJweA128GCM, KeyObjectInternal, MemberOf, @@ -94,6 +95,7 @@ impl From<EntryClass> for &'static str { EntryClass::KeyProviderInternal => ENTRYCLASS_KEY_PROVIDER_INTERNAL, EntryClass::KeyObject => ENTRYCLASS_KEY_OBJECT, EntryClass::KeyObjectJwtEs256 => ENTRYCLASS_KEY_OBJECT_JWT_ES256, + EntryClass::KeyObjectJwtRs256 => ENTRYCLASS_KEY_OBJECT_JWT_RS256, EntryClass::KeyObjectJweA128GCM => ENTRYCLASS_KEY_OBJECT_JWE_A128GCM, EntryClass::KeyObjectInternal => ENTRYCLASS_KEY_OBJECT_INTERNAL, EntryClass::MemberOf => ENTRYCLASS_MEMBER_OF, diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index fc6c2f286..2471aee40 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -334,6 +334,10 @@ pub const UUID_SCHEMA_ATTR_ACP_MODIFY_PRESENT_CLASS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000189"); pub const UUID_SCHEMA_ATTR_ACP_MODIFY_REMOVE_CLASS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000190"); +pub const UUID_SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_RS256: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000191"); +pub const UUID_SCHEMA_CLASS_KEY_OBJECT_JWT_RS256: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000192"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. diff --git a/server/lib/src/idm/oauth2.rs b/server/lib/src/idm/oauth2.rs index 2905abbd2..ca02884a1 100644 --- a/server/lib/src/idm/oauth2.rs +++ b/server/lib/src/idm/oauth2.rs @@ -4,57 +4,49 @@ //! integrations, which are then able to be used an accessed from the IDM layer //! for operations involving OAuth2 authentication processing. -use std::collections::btree_map::Entry as BTreeEntry; -use std::collections::{BTreeMap, BTreeSet}; -use std::convert::TryFrom; -use std::fmt; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; - +use crate::idm::account::Account; +use crate::idm::server::{ + IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction, +}; +use crate::prelude::*; +use crate::server::keys::{KeyObject, KeyProvidersTransaction, KeyProvidersWriteTransaction}; +use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE}; use base64::{engine::general_purpose, Engine as _}; -use hashbrown::HashSet; - pub use compact_jwt::{compact::JwkKeySet, OidcToken}; use compact_jwt::{ - crypto::JwsRs256Signer, jws::JwsBuilder, JwsCompact, JwsEs256Signer, JwsSigner, - JwsSignerToVerifier, JwsVerifier, OidcClaims, OidcSubject, + crypto::{JweA128GCMEncipher, JweA128KWEncipher}, + jwe::Jwe, + jws::JwsBuilder, + JweCompact, JwsCompact, OidcClaims, OidcSubject, }; use concread::cowcell::*; -use fernet::Fernet; use hashbrown::HashMap; +use hashbrown::HashSet; use kanidm_proto::constants::*; - -// #[cfg(feature = "dev-oauth2-device-flow")] -// use kanidm_proto::oauth2::OAUTH2_DEVICE_CODE_EXPIRY_SECONDS; - pub use kanidm_proto::oauth2::{ AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq, OAuth2RFC9068Token, OAuth2RFC9068TokenExtensions, Oauth2Rfc8414MetadataResponse, OidcDiscoveryResponse, OidcWebfingerRel, OidcWebfingerResponse, PkceAlg, TokenRevokeRequest, }; - use kanidm_proto::oauth2::{ AccessTokenType, ClaimType, DeviceAuthorizationResponse, DisplayValue, GrantType, IdTokenSignAlg, ResponseMode, ResponseType, SubjectType, TokenEndpointAuthMethod, }; use openssl::sha; - use serde::{Deserialize, Serialize}; use serde_with::{formats, serde_as}; +use std::collections::btree_map::Entry as BTreeEntry; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; use time::OffsetDateTime; use tracing::trace; use uri::{OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT}; use url::{Host, Origin, Url}; -use crate::idm::account::Account; -use crate::idm::server::{ - IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction, -}; -use crate::prelude::*; -use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE}; - #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "snake_case")] pub enum Oauth2Error { @@ -134,6 +126,8 @@ struct ConsentToken { pub client_id: String, // Must match the session id of the Uat, pub session_id: Uuid, + pub expiry: u64, + // So we can ensure that we really match the same uat to prevent confusions. pub ident_id: IdentityId, // CSRF @@ -158,10 +152,11 @@ struct ConsentToken { struct TokenExchangeCode { // We don't need the client_id here, because it's signed with an RS specific // key which gives us the assurance that it's the correct combination. - // pub uat: UserAuthToken, pub account_uuid: Uuid, pub session_id: Uuid, + pub expiry: u64, + // The S256 code challenge. #[serde_as( as = "Option<serde_with::base64::Base64<serde_with::base64::UrlSafe, formats::Unpadded>>" @@ -359,12 +354,6 @@ impl std::fmt::Debug for OauthRSType { } } -#[derive(Clone)] -enum Oauth2JwsSigner { - ES256 { signer: JwsEs256Signer }, - RS256 { signer: JwsRs256Signer }, -} - #[derive(Clone, Debug)] struct ClaimValue { join: OauthClaimMapJoin, @@ -398,6 +387,12 @@ impl ClaimValue { } } +#[derive(Clone, Copy, Debug)] +enum SignatureAlgo { + Es256, + Rs256, +} + #[derive(Clone)] pub struct Oauth2RS { name: String, @@ -416,8 +411,8 @@ pub struct Oauth2RS { client_scopes: BTreeSet<String>, client_sup_scopes: BTreeSet<String>, // Our internal exchange encryption material for this rs. - token_fernet: Fernet, - jws_signer: Oauth2JwsSigner, + sign_alg: SignatureAlgo, + key_object: Arc<KeyObject>, // For oidc we also need our issuer url. iss: Url, @@ -486,7 +481,7 @@ impl std::fmt::Debug for Oauth2RS { #[derive(Clone)] struct Oauth2RSInner { origin: Url, - fernet: Fernet, + consent_key: JweA128KWEncipher, rs_set: HashMap<String, Oauth2RS>, } @@ -502,31 +497,20 @@ pub struct Oauth2ResourceServersWriteTransaction<'a> { inner: CowCellWriteTxn<'a, Oauth2RSInner>, } -impl TryFrom<(Vec<Arc<EntrySealedCommitted>>, Url, DomainVersion)> for Oauth2ResourceServers { - type Error = OperationError; +impl Oauth2ResourceServers { + pub fn new(origin: Url) -> Result<Self, OperationError> { + let consent_key = JweA128KWEncipher::generate_ephemeral() + .map_err(|_| OperationError::CryptographyError)?; - fn try_from( - value: (Vec<Arc<EntrySealedCommitted>>, Url, DomainVersion), - ) -> Result<Self, Self::Error> { - let (value, origin, domain_level) = value; - let fernet = - Fernet::new(&Fernet::generate_key()).ok_or(OperationError::CryptographyError)?; - let oauth2rs = Oauth2ResourceServers { + Ok(Oauth2ResourceServers { inner: CowCell::new(Oauth2RSInner { origin, - fernet, + consent_key, rs_set: HashMap::new(), }), - }; - - let mut oauth2rs_wr = oauth2rs.write(); - oauth2rs_wr.reload(value, domain_level)?; - oauth2rs_wr.commit(); - Ok(oauth2rs) + }) } -} -impl Oauth2ResourceServers { pub fn read(&self) -> Oauth2ResourceServersReadTransaction { Oauth2ResourceServersReadTransaction { inner: self.inner.read(), @@ -544,6 +528,7 @@ impl Oauth2ResourceServersWriteTransaction<'_> { pub fn reload( &mut self, value: Vec<Arc<EntrySealedCommitted>>, + key_providers: &KeyProvidersWriteTransaction, domain_level: DomainVersion, ) -> Result<(), OperationError> { let rs_set: Result<HashMap<_, _>, _> = value @@ -552,13 +537,23 @@ impl Oauth2ResourceServersWriteTransaction<'_> { let uuid = ent.get_uuid(); trace!(?uuid, "Checking OAuth2 configuration"); // From each entry, attempt to make an OAuth2 configuration. - if !ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into()) { + if !ent + .attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into()) + { error!("Missing class oauth2_resource_server"); // Check we have oauth2_resource_server class return Err(OperationError::InvalidEntryState); } - let type_ = if ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerBasic.into()) { + let Some(key_object) = key_providers.get_key_object_handle(uuid) else { + error!("OAuth2 rs is missing it's key object!"); + return Err(OperationError::InvalidEntryState); + }; + + let type_ = if ent.attribute_equality( + Attribute::Class, + &EntryClass::OAuth2ResourceServerBasic.into(), + ) { let authz_secret = ent .get_ava_single_secret(Attribute::OAuth2RsBasicSecret) .map(str::to_string) @@ -573,13 +568,16 @@ impl Oauth2ResourceServersWriteTransaction<'_> { authz_secret, enable_pkce, } - } else if ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerPublic.into()) { + } else if ent.attribute_equality( + Attribute::Class, + &EntryClass::OAuth2ResourceServerPublic.into(), + ) { let allow_localhost_redirect = ent .get_ava_single_bool(Attribute::OAuth2AllowLocalhostRedirect) .unwrap_or(false); OauthRSType::Public { - allow_localhost_redirect + allow_localhost_redirect, } } else { error!("Missing class determining OAuth2 rs type"); @@ -597,7 +595,6 @@ impl Oauth2ResourceServersWriteTransaction<'_> { .map(str::to_string) .ok_or(OperationError::InvalidValueState)?; - // Setup the landing uri and its implied origin, as well as // the supplemental origins. let landing_url = ent @@ -605,14 +602,17 @@ impl Oauth2ResourceServersWriteTransaction<'_> { .cloned() .ok_or(OperationError::InvalidValueState)?; - let maybe_extra_urls = ent.get_ava_set(Attribute::OAuth2RsOrigin).and_then(|s| s.as_url_set()); + let maybe_extra_urls = ent + .get_ava_set(Attribute::OAuth2RsOrigin) + .and_then(|s| s.as_url_set()); let len_uris = maybe_extra_urls.map(|s| s.len() + 1).unwrap_or(1); // If we are DL8, then strict enforcement is always required. - let strict_redirect_uri = cfg!(test) || - domain_level >= DOMAIN_LEVEL_8 || - ent.get_ava_single_bool(Attribute::OAuth2StrictRedirectUri) + let strict_redirect_uri = cfg!(test) + || domain_level >= DOMAIN_LEVEL_8 + || ent + .get_ava_single_bool(Attribute::OAuth2StrictRedirectUri) .unwrap_or(false); // The reason we have to allocate this is that we need to do some processing on these @@ -651,13 +651,6 @@ impl Oauth2ResourceServersWriteTransaction<'_> { } } - let token_fernet = ent - .get_ava_single_secret(Attribute::OAuth2RsTokenKey) - .ok_or(OperationError::InvalidValueState) - .and_then(|key| { - Fernet::new(key).ok_or(OperationError::CryptographyError) - })?; - let scope_maps = ent .get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap) .cloned() @@ -670,38 +663,38 @@ impl Oauth2ResourceServersWriteTransaction<'_> { // From our scope maps we can now determine what scopes would be granted to our // client during a client credentials authentication. - let (client_scopes, client_sup_scopes) = if let Some(client_member_of) = ent.get_ava_refer(Attribute::MemberOf) { - let client_scopes = - scope_maps - .iter() - .filter_map(|(u, m)| { - if client_member_of.contains(u) { - Some(m.iter()) - } else { - None - } - }) - .flatten() - .cloned() - .collect::<BTreeSet<_>>(); + let (client_scopes, client_sup_scopes) = + if let Some(client_member_of) = ent.get_ava_refer(Attribute::MemberOf) { + let client_scopes = scope_maps + .iter() + .filter_map(|(u, m)| { + if client_member_of.contains(u) { + Some(m.iter()) + } else { + None + } + }) + .flatten() + .cloned() + .collect::<BTreeSet<_>>(); - let client_sup_scopes = sup_scope_maps - .iter() - .filter_map(|(u, m)| { - if client_member_of.contains(u) { - Some(m.iter()) - } else { - None - } - }) - .flatten() - .cloned() - .collect::<BTreeSet<_>>(); + let client_sup_scopes = sup_scope_maps + .iter() + .filter_map(|(u, m)| { + if client_member_of.contains(u) { + Some(m.iter()) + } else { + None + } + }) + .flatten() + .cloned() + .collect::<BTreeSet<_>>(); - (client_scopes, client_sup_scopes) - } else { - (BTreeSet::default(), BTreeSet::default()) - }; + (client_scopes, client_sup_scopes) + } else { + (BTreeSet::default(), BTreeSet::default()) + }; let e_claim_maps = ent .get_ava_set(Attribute::OAuth2RsClaimMap) @@ -720,23 +713,21 @@ impl Oauth2ResourceServersWriteTransaction<'_> { // to be unique. match claim_map.entry(*group_uuid) { BTreeEntry::Vacant(e) => { - e.insert( - vec![ - ( - claim_name.clone(), ClaimValue { - join: claim_mapping.join(), - values: claim_values.clone() - } - ) - ] - ); + e.insert(vec![( + claim_name.clone(), + ClaimValue { + join: claim_mapping.join(), + values: claim_values.clone(), + }, + )]); } BTreeEntry::Occupied(mut e) => { e.get_mut().push(( - claim_name.clone(), ClaimValue { - join: claim_mapping.join(), - values: claim_values.clone() - } + claim_name.clone(), + ClaimValue { + join: claim_mapping.join(), + values: claim_values.clone(), + }, )); } } @@ -748,33 +739,13 @@ impl Oauth2ResourceServersWriteTransaction<'_> { BTreeMap::default() }; - trace!("{}", Attribute::OAuth2JwtLegacyCryptoEnable); - let jws_signer = if ent.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false) { - trace!("{}", Attribute::Rs256PrivateKeyDer); - ent - .get_ava_single_private_binary(Attribute::Rs256PrivateKeyDer) - .ok_or(OperationError::InvalidValueState) - .and_then(|key_der| { - JwsRs256Signer::from_rs256_der(key_der) - .map(|signer| Oauth2JwsSigner::RS256 { signer }) - .map_err(|e| { - admin_error!(err = ?e, "Unable to load Legacy RS256 JwsSigner from DER"); - OperationError::CryptographyError - }) - })? + let sign_alg = if ent + .get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable) + .unwrap_or(false) + { + SignatureAlgo::Rs256 } else { - trace!("{}", Attribute::Es256PrivateKeyDer); - ent - .get_ava_single_private_binary(Attribute::Es256PrivateKeyDer) - .ok_or(OperationError::InvalidValueState) - .and_then(|key_der| { - JwsEs256Signer::from_es256_der(key_der) - .map(|signer| Oauth2JwsSigner::ES256 { signer }) - .map_err(|e| { - admin_error!(err = ?e, "Unable to load ES256 JwsSigner from DER"); - OperationError::CryptographyError - }) - })? + SignatureAlgo::Es256 }; let prefer_short_username = ent @@ -804,33 +775,31 @@ impl Oauth2ResourceServersWriteTransaction<'_> { let mut iss = self.inner.origin.clone(); iss.set_path(&format!("/oauth2/openid/{name}")); - let scopes_supported: BTreeSet<String> = - scope_maps + let scopes_supported: BTreeSet<String> = scope_maps .values() .flat_map(|bts| bts.iter()) - - .chain( - sup_scope_maps - .values() - .flat_map(|bts| bts.iter()) - ) - + .chain(sup_scope_maps.values().flat_map(|bts| bts.iter())) .cloned() .collect(); - - let device_authorization_endpoint: Option<Url> = match cfg!(feature="dev-oauth2-device-flow") { + let device_authorization_endpoint: Option<Url> = + match cfg!(feature = "dev-oauth2-device-flow") { true => { - match ent.get_ava_single_bool(Attribute::OAuth2DeviceFlowEnable).unwrap_or(false) { - true => { - let mut device_authorization_endpoint = self.inner.origin.clone(); - device_authorization_endpoint.set_path(uri::OAUTH2_AUTHORISE_DEVICE); - Some(device_authorization_endpoint) - }, - false => None + match ent + .get_ava_single_bool(Attribute::OAuth2DeviceFlowEnable) + .unwrap_or(false) + { + true => { + let mut device_authorization_endpoint = + self.inner.origin.clone(); + device_authorization_endpoint + .set_path(uri::OAUTH2_AUTHORISE_DEVICE); + Some(device_authorization_endpoint) + } + false => None, } - }, - false => {None} + } + false => None, }; let client_id = name.clone(); let rscfg = Oauth2RS { @@ -847,8 +816,8 @@ impl Oauth2ResourceServersWriteTransaction<'_> { client_scopes, client_sup_scopes, claim_map, - token_fernet, - jws_signer, + sign_alg, + key_object, iss, authorization_endpoint, token_endpoint, @@ -919,24 +888,19 @@ impl IdmServerProxyWriteTransaction<'_> { // are either signed *or* encrypted, we need to check both options. let (session_id, expiry, uuid) = if let Ok(jwsc) = JwsCompact::from_str(&revoke_req.token) { - let access_token = match &o2rs.jws_signer { - Oauth2JwsSigner::ES256 { signer } => signer - .get_verifier() - .and_then(|verifier| verifier.verify(&jwsc)), - Oauth2JwsSigner::RS256 { signer } => signer - .get_verifier() - .and_then(|verifier| verifier.verify(&jwsc)), - } - .map_err(|err| { - admin_error!(?err, "Unable to verify access token"); - Oauth2Error::InvalidRequest - }) - .and_then(|jws| { - jws.from_json().map_err(|err| { - admin_error!(?err, "Unable to deserialise access token"); + let access_token = o2rs + .key_object + .jws_verify(&jwsc) + .map_err(|err| { + admin_error!(?err, "Unable to verify access token"); Oauth2Error::InvalidRequest }) - })?; + .and_then(|jws| { + jws.from_json().map_err(|err| { + admin_error!(?err, "Unable to deserialise access token"); + Oauth2Error::InvalidRequest + }) + })?; let OAuth2RFC9068Token::<_> { sub: uuid, @@ -948,17 +912,21 @@ impl IdmServerProxyWriteTransaction<'_> { (session_id, exp, uuid) } else { // Assume it's encrypted. + let jwe_compact = JweCompact::from_str(&revoke_req.token).map_err(|_| { + error!("Failed to deserialise a valid JWE"); + Oauth2Error::InvalidRequest + })?; let token: Oauth2TokenType = o2rs - .token_fernet - .decrypt(&revoke_req.token) + .key_object + .jwe_decrypt(&jwe_compact) .map_err(|_| { - admin_error!("Failed to decrypt token revoke request"); + error!("Failed to decrypt token revoke request"); Oauth2Error::InvalidRequest }) - .and_then(|data| { - serde_json::from_slice(&data).map_err(|e| { - admin_error!("Failed to deserialise token - {:?}", e); + .and_then(|jwe| { + jwe.from_json().map_err(|err| { + error!(?err, "Failed to deserialise token"); Oauth2Error::InvalidRequest }) })?; @@ -1185,19 +1153,23 @@ impl IdmServerProxyWriteTransaction<'_> { return Err(OperationError::InvalidSessionState); }; - // Decode the consent req with our system fernet key. Use a ttl of 5 minutes. + let consent_token_jwe = JweCompact::from_str(consent_token).map_err(|err| { + error!(?err, "Consent token is not a valid jwe compact"); + OperationError::InvalidSessionState + })?; + let consent_req: ConsentToken = self .oauth2rs .inner - .fernet - .decrypt_at_time(consent_token, Some(300), ct.as_secs()) - .map_err(|_| { - admin_error!("Failed to decrypt consent request"); + .consent_key + .decipher(&consent_token_jwe) + .map_err(|err| { + error!(?err, "Failed to decrypt consent request"); OperationError::CryptographyError }) - .and_then(|data| { - serde_json::from_slice(&data).map_err(|e| { - admin_error!(err = ?e, "Failed to deserialise consent request"); + .and_then(|jwe| { + jwe.from_json().map_err(|err| { + error!(?err, "Failed to deserialise consent request"); OperationError::SerdeJsonError }) })?; @@ -1214,6 +1186,15 @@ impl IdmServerProxyWriteTransaction<'_> { return Err(OperationError::InvalidSessionState); } + if consent_req.expiry <= ct.as_secs() { + // Token is expired + error!("Failed to decrypt consent request"); + return Err(OperationError::CryptographyError); + } + + // The exchange must be performed in the next 60 seconds. + let expiry = ct.as_secs() + 60; + // Get the resource server config based on this client_id. let o2rs = self .oauth2rs @@ -1227,22 +1208,29 @@ impl IdmServerProxyWriteTransaction<'_> { // Extract the state, code challenge, redirect_uri let xchg_code = TokenExchangeCode { - // uat: uat.clone(), account_uuid, session_id: ident.get_session_id(), + expiry, code_challenge: consent_req.code_challenge, redirect_uri: consent_req.redirect_uri.clone(), scopes: consent_req.scopes.clone(), nonce: consent_req.nonce, }; - // Encrypt the exchange token with the fernet key of the client resource server - let code_data = serde_json::to_vec(&xchg_code).map_err(|e| { - admin_error!(err = ?e, "Unable to encode xchg_code data"); + // Encrypt the exchange token + let code_data_jwe = Jwe::into_json(&xchg_code).map_err(|err| { + error!(?err, "Unable to encode xchg_code data"); OperationError::SerdeJsonError })?; - let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs()); + let code = o2rs + .key_object + .jwe_a128gcm_encrypt(&code_data_jwe, ct) + .map(|code| code.to_string()) + .map_err(|err| { + error!(?err, "Unable to encrypt xchg_code"); + OperationError::CryptographyError + })?; // Everything is DONE! Now submit that it's all happy and the user consented correctly. // this will let them bypass consent steps in the future. @@ -1283,21 +1271,31 @@ impl IdmServerProxyWriteTransaction<'_> { ) -> Result<AccessTokenResponse, Oauth2Error> { // Check the token_req is within the valid time, and correctly signed for // this client. + let jwe_compact = JweCompact::from_str(token_req_code).map_err(|_| { + error!("Failed to deserialise a valid JWE"); + Oauth2Error::InvalidRequest + })?; let code_xchg: TokenExchangeCode = o2rs - .token_fernet - .decrypt_at_time(token_req_code, Some(60), ct.as_secs()) + .key_object + .jwe_decrypt(&jwe_compact) .map_err(|_| { admin_error!("Failed to decrypt token exchange request"); Oauth2Error::InvalidRequest }) - .and_then(|data| { - serde_json::from_slice(&data).map_err(|e| { - admin_error!("Failed to deserialise token exchange code - {:?}", e); + .and_then(|jwe| { + debug!(?jwe); + jwe.from_json::<TokenExchangeCode>().map_err(|err| { + error!(?err, "Failed to deserialise token exchange code"); Oauth2Error::InvalidRequest }) })?; + if code_xchg.expiry <= ct.as_secs() { + error!("Expired token exchange request"); + return Err(Oauth2Error::InvalidRequest); + } + // If we have a verifier present, we MUST assert that a code challenge is present! // It is worth noting here that code_xchg is *server issued* and encrypted, with // a short validity period. The client controlled value is in token_req.code_verifier @@ -1380,17 +1378,22 @@ impl IdmServerProxyWriteTransaction<'_> { req_scopes: Option<&BTreeSet<String>>, ct: Duration, ) -> Result<AccessTokenResponse, Oauth2Error> { + let jwe_compact = JweCompact::from_str(refresh_token).map_err(|_| { + error!("Failed to deserialise a valid JWE"); + Oauth2Error::InvalidRequest + })?; + // Validate the refresh token decrypts and it's expiry is within the valid window. let token: Oauth2TokenType = o2rs - .token_fernet - .decrypt(refresh_token) + .key_object + .jwe_decrypt(&jwe_compact) .map_err(|_| { admin_error!("Failed to decrypt refresh token request"); Oauth2Error::InvalidRequest }) - .and_then(|data| { - serde_json::from_slice(&data).map_err(|e| { - admin_error!("Failed to deserialise token - {:?}", e); + .and_then(|jwe| { + jwe.from_json().map_err(|err| { + error!(?err, "Failed to deserialise token"); Oauth2Error::InvalidRequest }) })?; @@ -1568,14 +1571,19 @@ impl IdmServerProxyWriteTransaction<'_> { nbf: iat, }; - let access_token_data = serde_json::to_vec(&access_token_raw).map_err(|e| { - admin_error!(err = ?e, "Unable to encode token data"); + let access_token_data = Jwe::into_json(&access_token_raw).map_err(|err| { + error!(?err, "Unable to encode token data"); Oauth2Error::ServerError(OperationError::SerdeJsonError) })?; let access_token = o2rs - .token_fernet - .encrypt_at_time(&access_token_data, ct.as_secs()); + .key_object + .jwe_a128gcm_encrypt(&access_token_data, ct) + .map(|jwe| jwe.to_string()) + .map_err(|err| { + error!(?err, "Unable to encode token data"); + Oauth2Error::ServerError(OperationError::CryptographyError) + })?; // Write the session to the db let session = Value::Oauth2Session( @@ -1703,13 +1711,19 @@ impl IdmServerProxyWriteTransaction<'_> { }; trace!(?oidc); + let oidc = JwsBuilder::into_json(&oidc) + .map(|builder| builder.build()) + .map_err(|err| { + admin_error!(?err, "Unable to encode access token data"); + Oauth2Error::ServerError(OperationError::InvalidState) + })?; - let jwt_signed = match &o2rs.jws_signer { - Oauth2JwsSigner::ES256 { signer } => signer.sign(&oidc), - Oauth2JwsSigner::RS256 { signer } => signer.sign(&oidc), + let jwt_signed = match o2rs.sign_alg { + SignatureAlgo::Es256 => o2rs.key_object.jws_es256_sign(&oidc, ct), + SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_sign(&oidc, ct), } - .map_err(|e| { - admin_error!(err = ?e, "Unable to encode uat data"); + .map_err(|err| { + error!(?err, "Unable to encode oidc token data"); Oauth2Error::ServerError(OperationError::InvalidState) })?; @@ -1742,14 +1756,14 @@ impl IdmServerProxyWriteTransaction<'_> { let access_token_data = JwsBuilder::into_json(&access_token_data) .map(|builder| builder.set_typ(Some("at+jwt")).build()) - .map_err(|e| { - admin_error!(err = ?e, "Unable to encode access token data"); + .map_err(|err| { + error!(?err, "Unable to encode access token data"); Oauth2Error::ServerError(OperationError::InvalidState) })?; - let access_token = match &o2rs.jws_signer { - Oauth2JwsSigner::ES256 { signer } => signer.sign(&access_token_data), - Oauth2JwsSigner::RS256 { signer } => signer.sign(&access_token_data), + let access_token = match o2rs.sign_alg { + SignatureAlgo::Es256 => o2rs.key_object.jws_es256_sign(&access_token_data, ct), + SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_sign(&access_token_data, ct), } .map_err(|e| { admin_error!(err = ?e, "Unable to sign access token data"); @@ -1767,14 +1781,19 @@ impl IdmServerProxyWriteTransaction<'_> { nonce, }; - let refresh_token_data = serde_json::to_vec(&refresh_token_raw).map_err(|e| { - admin_error!(err = ?e, "Unable to encode token data"); + let refresh_token_data = Jwe::into_json(&refresh_token_raw).map_err(|err| { + error!(?err, "Unable to encode token data"); Oauth2Error::ServerError(OperationError::SerdeJsonError) })?; let refresh_token = o2rs - .token_fernet - .encrypt_at_time(&refresh_token_data, ct.as_secs()); + .key_object + .jwe_a128gcm_encrypt(&refresh_token_data, ct) + .map(|jwe| jwe.to_string()) + .map_err(|err| { + error!(?err, "Unable to encrypt token data"); + Oauth2Error::ServerError(OperationError::CryptographyError) + })?; // Write the session to the db even with the refresh path, we need to do // this to update the "not issued before" time. @@ -1846,15 +1865,20 @@ impl IdmServerProxyWriteTransaction<'_> { } } - o2rs.token_fernet - .decrypt(token) - .map_err(|_| { - admin_error!("Failed to decrypt token reflection request"); + let jwe_compact = JweCompact::from_str(token).map_err(|err| { + error!(?err, "Failed to deserialise a valid JWE"); + OperationError::InvalidSessionState + })?; + + o2rs.key_object + .jwe_decrypt(&jwe_compact) + .map_err(|err| { + error!(?err, "Failed to decrypt token reflection request"); OperationError::CryptographyError }) - .and_then(|data| { - serde_json::from_slice(&data).map_err(|e| { - admin_error!("Failed to deserialise token exchange code - {:?}", e); + .and_then(|jwe| { + jwe.from_json().map_err(|err| { + error!(?err, "Failed to deserialise token for reflection"); OperationError::SerdeJsonError }) }) @@ -2129,23 +2153,34 @@ impl IdmServerProxyReadTransaction<'_> { ); } + // Xchg token expires in + let expiry = ct.as_secs() + 60; + // Setup for the permit success let xchg_code = TokenExchangeCode { account_uuid, session_id, + expiry, code_challenge, redirect_uri: auth_req.redirect_uri.clone(), scopes: granted_scopes.into_iter().collect(), nonce: auth_req.nonce.clone(), }; - // Encrypt the exchange token with the fernet key of the client resource server - let code_data = serde_json::to_vec(&xchg_code).map_err(|e| { - admin_error!(err = ?e, "Unable to encode xchg_code data"); + // Encrypt the exchange token with the key of the client + let code_data_jwe = Jwe::into_json(&xchg_code).map_err(|err| { + error!(?err, "Unable to encode xchg_code data"); Oauth2Error::ServerError(OperationError::SerdeJsonError) })?; - let code = o2rs.token_fernet.encrypt_at_time(&code_data, ct.as_secs()); + let code = o2rs + .key_object + .jwe_a128gcm_encrypt(&code_data_jwe, ct) + .map(|jwe| jwe.to_string()) + .map_err(|err| { + error!(?err, "Unable to encrypt xchg_code data"); + Oauth2Error::ServerError(OperationError::CryptographyError) + })?; Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess { redirect_uri: auth_req.redirect_uri.clone(), @@ -2180,6 +2215,9 @@ impl IdmServerProxyReadTransaction<'_> { pii_scopes.insert(OAUTH2_SCOPE_SSH_PUBLICKEYS.to_string()); } + // Consent token expires in + let expiry = ct.as_secs() + 300; + // Subsequent we then return an encrypted session handle which allows // the user to indicate their consent to this authorisation. // @@ -2188,6 +2226,7 @@ impl IdmServerProxyReadTransaction<'_> { let consent_req = ConsentToken { client_id: auth_req.client_id.clone(), ident_id: ident.get_event_origin_id(), + expiry, session_id, state: auth_req.state.clone(), code_challenge, @@ -2197,16 +2236,21 @@ impl IdmServerProxyReadTransaction<'_> { response_mode, }; - let consent_data = serde_json::to_vec(&consent_req).map_err(|e| { - admin_error!(err = ?e, "Unable to encode consent data"); + let consent_jwe = Jwe::into_json(&consent_req).map_err(|err| { + error!(?err, "Unable to encode consent data"); Oauth2Error::ServerError(OperationError::SerdeJsonError) })?; let consent_token = self .oauth2rs .inner - .fernet - .encrypt_at_time(&consent_data, ct.as_secs()); + .consent_key + .encipher::<JweA128GCMEncipher>(&consent_jwe) + .map(|jwe_compact| jwe_compact.to_string()) + .map_err(|err| { + error!(?err, "Unable to encrypt jwe"); + Oauth2Error::ServerError(OperationError::CryptographyError) + })?; Ok(AuthoriseResponse::ConsentRequested { client_name: o2rs.displayname.clone(), @@ -2224,19 +2268,24 @@ impl IdmServerProxyReadTransaction<'_> { consent_token: &str, ct: Duration, ) -> Result<AuthoriseReject, OperationError> { + let jwe_compact = JweCompact::from_str(consent_token).map_err(|_| { + error!("Failed to deserialise a valid JWE"); + OperationError::CryptographyError + })?; + // Decode the consent req with our system fernet key. Use a ttl of 5 minutes. let consent_req: ConsentToken = self .oauth2rs .inner - .fernet - .decrypt_at_time(consent_token, Some(300), ct.as_secs()) + .consent_key + .decipher(&jwe_compact) .map_err(|_| { admin_error!("Failed to decrypt consent request"); OperationError::CryptographyError }) - .and_then(|data| { - serde_json::from_slice(&data).map_err(|e| { - admin_error!(err = ?e, "Failed to deserialise consent request"); + .and_then(|jwe| { + jwe.from_json().map_err(|err| { + error!(?err, "Failed to deserialise consent request"); OperationError::SerdeJsonError }) })?; @@ -2253,6 +2302,12 @@ impl IdmServerProxyReadTransaction<'_> { return Err(OperationError::InvalidSessionState); } + if consent_req.expiry <= ct.as_secs() { + // Token is expired + error!("Failed to decrypt consent request"); + return Err(OperationError::CryptographyError); + } + // Get the resource server config based on this client_id. let _o2rs = self .oauth2rs @@ -2308,24 +2363,19 @@ impl IdmServerProxyReadTransaction<'_> { let prefer_short_username = o2rs.prefer_short_username; if let Ok(jwsc) = JwsCompact::from_str(&intr_req.token) { - let access_token = match &o2rs.jws_signer { - Oauth2JwsSigner::ES256 { signer } => signer - .get_verifier() - .and_then(|verifier| verifier.verify(&jwsc)), - Oauth2JwsSigner::RS256 { signer } => signer - .get_verifier() - .and_then(|verifier| verifier.verify(&jwsc)), - } - .map_err(|err| { - admin_error!(?err, "Unable to verify access token"); - Oauth2Error::InvalidRequest - }) - .and_then(|jws| { - jws.from_json().map_err(|err| { - admin_error!(?err, "Unable to deserialise access token"); + let access_token = o2rs + .key_object + .jws_verify(&jwsc) + .map_err(|err| { + error!(?err, "Unable to verify access token"); Oauth2Error::InvalidRequest }) - })?; + .and_then(|jws| { + jws.from_json().map_err(|err| { + error!(?err, "Unable to deserialise access token"); + Oauth2Error::InvalidRequest + }) + })?; let OAuth2RFC9068Token::<_> { iss: _, @@ -2398,16 +2448,21 @@ impl IdmServerProxyReadTransaction<'_> { jti: None, }) } else { + let jwe_compact = JweCompact::from_str(&intr_req.token).map_err(|_| { + error!("Failed to deserialise a valid JWE"); + Oauth2Error::InvalidRequest + })?; + let token: Oauth2TokenType = o2rs - .token_fernet - .decrypt(&intr_req.token) + .key_object + .jwe_decrypt(&jwe_compact) .map_err(|_| { admin_error!("Failed to decrypt token introspection request"); Oauth2Error::InvalidRequest }) - .and_then(|data| { - serde_json::from_slice(&data).map_err(|e| { - admin_error!("Failed to deserialise token - {:?}", e); + .and_then(|jwe| { + jwe.from_json().map_err(|err| { + error!(?err, "Failed to deserialise token"); Oauth2Error::InvalidRequest }) })?; @@ -2495,24 +2550,19 @@ impl IdmServerProxyReadTransaction<'_> { &*(s as *const _) }; - let access_token = match &o2rs.jws_signer { - Oauth2JwsSigner::ES256 { signer } => signer - .get_verifier() - .and_then(|verifier| verifier.verify(&token)), - Oauth2JwsSigner::RS256 { signer } => signer - .get_verifier() - .and_then(|verifier| verifier.verify(&token)), - } - .map_err(|err| { - admin_error!(?err, "Unable to verify access token"); - Oauth2Error::InvalidRequest - }) - .and_then(|jws| { - jws.from_json().map_err(|err| { - admin_error!(?err, "Unable to deserialise access token"); + let access_token = o2rs + .key_object + .jws_verify(&token) + .map_err(|err| { + error!(?err, "Unable to verify access token"); Oauth2Error::InvalidRequest }) - })?; + .and_then(|jws| { + jws.from_json().map_err(|err| { + error!(?err, "Unable to deserialise access token"); + Oauth2Error::InvalidRequest + }) + })?; let OAuth2RFC9068Token::<_> { iss: _, @@ -2685,9 +2735,9 @@ impl IdmServerProxyReadTransaction<'_> { let subject_types_supported = vec![SubjectType::Public]; - let id_token_signing_alg_values_supported = match &o2rs.jws_signer { - Oauth2JwsSigner::ES256 { .. } => vec![IdTokenSignAlg::ES256], - Oauth2JwsSigner::RS256 { .. } => vec![IdTokenSignAlg::RS256], + let id_token_signing_alg_values_supported = match &o2rs.sign_alg { + SignatureAlgo::Es256 => vec![IdTokenSignAlg::ES256], + SignatureAlgo::Rs256 => vec![IdTokenSignAlg::RS256], }; let userinfo_signing_alg_values_supported = None; @@ -2818,15 +2868,18 @@ impl IdmServerProxyReadTransaction<'_> { OperationError::NoMatchingEntries })?; - match &o2rs.jws_signer { - Oauth2JwsSigner::ES256 { signer } => signer.public_key_as_jwk(), - Oauth2JwsSigner::RS256 { signer } => signer.public_key_as_jwk(), + // How do we return only the active signing algo types? + + error!(sign_alg = ?o2rs.sign_alg); + + match o2rs.sign_alg { + SignatureAlgo::Es256 => o2rs.key_object.jws_es256_jwks(), + SignatureAlgo::Rs256 => o2rs.key_object.jws_rs256_jwks(), } - .map_err(|e| { - admin_error!("Unable to retrieve public key for {} - {:?}", o2rs.name, e); + .ok_or_else(|| { + error!(o2_client = ?o2rs.name, "Unable to retrieve public keys"); OperationError::InvalidState }) - .map(|jwk| JwkKeySet { keys: vec![jwk] }) } } @@ -4699,11 +4752,11 @@ mod tests { ); // Invalid consent token - assert!( + assert_eq!( idms_prox_read .check_oauth2_authorise_reject(&ident, "not a token", ct) - .unwrap_err() - == OperationError::CryptographyError + .unwrap_err(), + OperationError::CryptographyError ); // Wrong ident diff --git a/server/lib/src/idm/server.rs b/server/lib/src/idm/server.rs index d047bb319..911150bfb 100644 --- a/server/lib/src/idm/server.rs +++ b/server/lib/src/idm/server.rs @@ -1,28 +1,3 @@ -use std::convert::TryFrom; -use std::sync::Arc; -use std::time::Duration; - -use kanidm_lib_crypto::CryptoPolicy; - -use compact_jwt::{Jwk, JwsCompact}; -use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn}; -use concread::cowcell::CowCellReadTxn; -use concread::hashmap::HashMap; -use kanidm_proto::internal::{ - ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, ScimSyncToken, - UatPurpose, UserAuthToken, -}; -use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; -use rand::prelude::*; -use tokio::sync::mpsc::{ - unbounded_channel as unbounded, UnboundedReceiver as Receiver, UnboundedSender as Sender, -}; -use tokio::sync::{Mutex, Semaphore}; -use tracing::trace; -use url::Url; -use webauthn_rs::prelude::{Webauthn, WebauthnBuilder}; -use zxcvbn::{zxcvbn, Score}; - use super::event::ReadBackupCodeEvent; use super::ldap::{LdapBoundToken, LdapSession}; use crate::credential::{softlock::CredSoftLock, Credential}; @@ -38,9 +13,6 @@ use crate::idm::delayed::{ AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, UnixPasswordUpgrade, WebauthnCounterIncrement, }; - -#[cfg(test)] -use crate::idm::event::PasswordChangeEvent; use crate::idm::event::{AuthEvent, AuthEventStep, AuthResult}; use crate::idm::event::{ CredentialStatusEvent, LdapAuthEvent, LdapTokenAuthEvent, RadiusAuthTokenEvent, @@ -61,6 +33,31 @@ use crate::server::keys::KeyProvidersTransaction; use crate::server::DomainInfo; use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid}; use crate::value::{Session, SessionState}; +use compact_jwt::{Jwk, JwsCompact}; +use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn}; +use concread::cowcell::CowCellReadTxn; +use concread::hashmap::HashMap; +use kanidm_lib_crypto::CryptoPolicy; +use kanidm_proto::internal::{ + ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, ScimSyncToken, + UatPurpose, UserAuthToken, +}; +use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; +use rand::prelude::*; +use std::convert::TryFrom; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::mpsc::{ + unbounded_channel as unbounded, UnboundedReceiver as Receiver, UnboundedSender as Sender, +}; +use tokio::sync::{Mutex, Semaphore}; +use tracing::trace; +use url::Url; +use webauthn_rs::prelude::{Webauthn, WebauthnBuilder}; +use zxcvbn::{zxcvbn, Score}; + +#[cfg(test)] +use crate::idm::event::PasswordChangeEvent; pub(crate) type AuthSessionMutex = Arc<Mutex<AuthSession>>; pub(crate) type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>; @@ -158,14 +155,12 @@ impl IdmServer { let (audit_tx, audit_rx) = unbounded(); // Get the domain name, as the relying party id. - let (rp_id, rp_name, domain_level, oauth2rs_set, application_set) = { + let (rp_id, rp_name, application_set) = { let mut qs_read = qs.read().await?; ( qs_read.get_domain_name().to_string(), qs_read.get_domain_display_name().to_string(), - qs_read.get_domain_version(), // Add a read/reload of all oauth2 configurations. - qs_read.get_oauth2rs_set()?, qs_read.get_applications_set()?, ) }; @@ -201,11 +196,10 @@ impl IdmServer { OperationError::InvalidState })?; - let oauth2rs = Oauth2ResourceServers::try_from((oauth2rs_set, origin_url, domain_level)) - .map_err(|e| { - admin_error!("Failed to load oauth2 resource servers - {:?}", e); - e - })?; + let oauth2rs = Oauth2ResourceServers::new(origin_url).map_err(|err| { + error!(?err, "Failed to load oauth2 resource servers"); + err + })?; let applications = LdapApplications::try_from(application_set).map_err(|e| { admin_error!("Failed to load ldap applications - {:?}", e); @@ -2121,6 +2115,12 @@ impl IdmServerProxyWriteTransaction<'_> { #[instrument(level = "debug", skip_all)] pub fn commit(mut self) -> Result<(), OperationError> { + // The problem we have here is that we need the qs_write layer to reload *first* + // so that things like schema and key objects are ready. + self.qs_write.reload()?; + + // Now that's done, let's proceed. + if self.qs_write.get_changed_app() { self.qs_write .get_applications_set() @@ -2128,9 +2128,11 @@ impl IdmServerProxyWriteTransaction<'_> { } if self.qs_write.get_changed_oauth2() { let domain_level = self.qs_write.get_domain_version(); - self.qs_write - .get_oauth2rs_set() - .and_then(|oauth2rs_set| self.oauth2rs.reload(oauth2rs_set, domain_level))?; + self.qs_write.get_oauth2rs_set().and_then(|oauth2rs_set| { + let key_providers = self.qs_write.get_key_providers(); + self.oauth2rs + .reload(oauth2rs_set, key_providers, domain_level) + })?; // Clear the flag to indicate we completed the reload. self.qs_write.clear_changed_oauth2(); } diff --git a/server/lib/src/migration_data/dl10/access.rs b/server/lib/src/migration_data/dl10/access.rs index 3e56613fc..c1b7da233 100644 --- a/server/lib/src/migration_data/dl10/access.rs +++ b/server/lib/src/migration_data/dl10/access.rs @@ -614,315 +614,7 @@ lazy_static! { } lazy_static! { - pub static ref IDM_ACP_OAUTH2_MANAGE_DL4: BuiltinAcp = BuiltinAcp { - classes: vec![ - EntryClass::Object, - EntryClass::AccessControlProfile, - EntryClass::AccessControlCreate, - EntryClass::AccessControlDelete, - EntryClass::AccessControlModify, - EntryClass::AccessControlSearch - ], - name: "idm_acp_hp_oauth2_manage_priv", - uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1, - description: "Builtin IDM Control for managing oauth2 resource server integrations.", - receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]), - target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![ - match_class_filter!(EntryClass::OAuth2ResourceServer), - FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(), - ])), - search_attrs: vec![ - Attribute::Class, - Attribute::Description, - Attribute::DisplayName, - Attribute::OAuth2RsName, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsBasicSecret, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::Rs256PrivateKeyDer, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - ], - modify_removed_attrs: vec![ - Attribute::Description, - Attribute::DisplayName, - Attribute::OAuth2RsName, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsBasicSecret, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::Rs256PrivateKeyDer, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - ], - modify_present_attrs: vec![ - Attribute::Description, - Attribute::DisplayName, - Attribute::OAuth2RsName, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - ], - create_attrs: vec![ - Attribute::Class, - Attribute::Description, - Attribute::DisplayName, - Attribute::OAuth2RsName, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - ], - create_classes: vec![ - EntryClass::Object, - EntryClass::OAuth2ResourceServer, - EntryClass::OAuth2ResourceServerBasic, - EntryClass::OAuth2ResourceServerPublic, - ], - ..Default::default() - }; -} - -lazy_static! { - pub static ref IDM_ACP_OAUTH2_MANAGE_DL5: BuiltinAcp = BuiltinAcp { - classes: vec![ - EntryClass::Object, - EntryClass::AccessControlProfile, - EntryClass::AccessControlCreate, - EntryClass::AccessControlDelete, - EntryClass::AccessControlModify, - EntryClass::AccessControlSearch - ], - name: "idm_acp_hp_oauth2_manage_priv", - uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1, - description: "Builtin IDM Control for managing oauth2 resource server integrations.", - receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]), - target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![ - match_class_filter!(EntryClass::OAuth2ResourceServer), - FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(), - ])), - search_attrs: vec![ - Attribute::Class, - Attribute::Description, - Attribute::DisplayName, - Attribute::Name, - Attribute::Spn, - Attribute::OAuth2Session, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsBasicSecret, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::Rs256PrivateKeyDer, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - ], - modify_removed_attrs: vec![ - Attribute::Description, - Attribute::DisplayName, - Attribute::Name, - Attribute::OAuth2Session, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsBasicSecret, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::Rs256PrivateKeyDer, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - ], - modify_present_attrs: vec![ - Attribute::Description, - Attribute::DisplayName, - Attribute::Name, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - ], - create_attrs: vec![ - Attribute::Class, - Attribute::Description, - Attribute::Name, - Attribute::DisplayName, - Attribute::OAuth2RsName, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - ], - create_classes: vec![ - EntryClass::Object, - EntryClass::Account, - EntryClass::OAuth2ResourceServer, - EntryClass::OAuth2ResourceServerBasic, - EntryClass::OAuth2ResourceServerPublic, - ], - ..Default::default() - }; -} - -lazy_static! { - pub static ref IDM_ACP_OAUTH2_MANAGE_DL7: BuiltinAcp = BuiltinAcp { - classes: vec![ - EntryClass::Object, - EntryClass::AccessControlProfile, - EntryClass::AccessControlCreate, - EntryClass::AccessControlDelete, - EntryClass::AccessControlModify, - EntryClass::AccessControlSearch - ], - name: "idm_acp_hp_oauth2_manage_priv", - uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1, - description: "Builtin IDM Control for managing oauth2 resource server integrations.", - receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]), - target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![ - match_class_filter!(EntryClass::OAuth2ResourceServer), - FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(), - ])), - search_attrs: vec![ - Attribute::Class, - Attribute::Description, - Attribute::DisplayName, - Attribute::Name, - Attribute::Spn, - Attribute::OAuth2Session, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsBasicSecret, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::Rs256PrivateKeyDer, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - Attribute::OAuth2StrictRedirectUri, - ], - modify_removed_attrs: vec![ - Attribute::Description, - Attribute::DisplayName, - Attribute::Name, - Attribute::OAuth2Session, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsBasicSecret, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::Rs256PrivateKeyDer, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - Attribute::OAuth2StrictRedirectUri, - ], - modify_present_attrs: vec![ - Attribute::Description, - Attribute::DisplayName, - Attribute::Name, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - Attribute::OAuth2StrictRedirectUri, - ], - create_attrs: vec![ - Attribute::Class, - Attribute::Description, - Attribute::Name, - Attribute::DisplayName, - Attribute::OAuth2RsName, - Attribute::OAuth2RsOrigin, - Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsSupScopeMap, - Attribute::OAuth2RsScopeMap, - Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::OAuth2JwtLegacyCryptoEnable, - Attribute::OAuth2PreferShortUsername, - Attribute::OAuth2AllowLocalhostRedirect, - Attribute::OAuth2RsClaimMap, - Attribute::Image, - Attribute::OAuth2StrictRedirectUri, - ], - create_classes: vec![ - EntryClass::Object, - EntryClass::Account, - EntryClass::OAuth2ResourceServer, - EntryClass::OAuth2ResourceServerBasic, - EntryClass::OAuth2ResourceServerPublic, - ], - ..Default::default() - }; -} - -lazy_static! { - pub static ref IDM_ACP_OAUTH2_MANAGE_DL9: BuiltinAcp = BuiltinAcp { + pub static ref IDM_ACP_OAUTH2_MANAGE: BuiltinAcp = BuiltinAcp { classes: vec![ EntryClass::Object, EntryClass::AccessControlProfile, @@ -951,10 +643,7 @@ lazy_static! { Attribute::OAuth2RsScopeMap, Attribute::OAuth2RsSupScopeMap, Attribute::OAuth2RsBasicSecret, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::Rs256PrivateKeyDer, Attribute::OAuth2JwtLegacyCryptoEnable, Attribute::OAuth2PreferShortUsername, Attribute::OAuth2AllowLocalhostRedirect, @@ -962,6 +651,7 @@ lazy_static! { Attribute::Image, Attribute::OAuth2StrictRedirectUri, Attribute::OAuth2DeviceFlowEnable, + Attribute::KeyInternalData, ], modify_removed_attrs: vec![ Attribute::Description, @@ -973,10 +663,7 @@ lazy_static! { Attribute::OAuth2RsScopeMap, Attribute::OAuth2RsSupScopeMap, Attribute::OAuth2RsBasicSecret, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, Attribute::OAuth2AllowInsecureClientDisablePkce, - Attribute::Rs256PrivateKeyDer, Attribute::OAuth2JwtLegacyCryptoEnable, Attribute::OAuth2PreferShortUsername, Attribute::OAuth2AllowLocalhostRedirect, @@ -984,6 +671,8 @@ lazy_static! { Attribute::Image, Attribute::OAuth2StrictRedirectUri, Attribute::OAuth2DeviceFlowEnable, + Attribute::KeyActionRevoke, + Attribute::KeyActionRotate, ], modify_present_attrs: vec![ Attribute::Description, @@ -1001,6 +690,8 @@ lazy_static! { Attribute::Image, Attribute::OAuth2StrictRedirectUri, Attribute::OAuth2DeviceFlowEnable, + Attribute::KeyActionRevoke, + Attribute::KeyActionRotate, ], create_attrs: vec![ Attribute::Class, @@ -1032,122 +723,6 @@ lazy_static! { }; } -lazy_static! { - pub static ref IDM_ACP_DOMAIN_ADMIN_DL6: BuiltinAcp = BuiltinAcp { - classes: vec![ - EntryClass::Object, - EntryClass::AccessControlProfile, - EntryClass::AccessControlModify, - EntryClass::AccessControlSearch - ], - name: "idm_acp_domain_admin", - uuid: UUID_IDM_ACP_DOMAIN_ADMIN_V1, - description: "Builtin IDM Control for granting domain info administration locally", - receiver: BuiltinAcpReceiver::Group(vec![UUID_DOMAIN_ADMINS]), - target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![ - ProtoFilter::Eq( - Attribute::Uuid.to_string(), - STR_UUID_DOMAIN_INFO.to_string() - ), - FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone() - ])), - search_attrs: vec![ - Attribute::Class, - Attribute::Name, - Attribute::Uuid, - Attribute::DomainDisplayName, - Attribute::DomainName, - Attribute::DomainLdapBasedn, - Attribute::LdapMaxQueryableAttrs, - Attribute::DomainSsid, - Attribute::DomainUuid, - // Grants read access to the key object. - // But this means we have to specify every type of key object? - // Future william problem ... - Attribute::KeyInternalData, - Attribute::LdapAllowUnixPwBind, - Attribute::Version, - ], - modify_removed_attrs: vec![ - Attribute::DomainDisplayName, - Attribute::DomainSsid, - Attribute::DomainLdapBasedn, - Attribute::LdapMaxQueryableAttrs, - Attribute::LdapAllowUnixPwBind, - Attribute::KeyActionRevoke, - Attribute::KeyActionRotate, - ], - modify_present_attrs: vec![ - Attribute::DomainDisplayName, - Attribute::DomainLdapBasedn, - Attribute::LdapMaxQueryableAttrs, - Attribute::DomainSsid, - Attribute::LdapAllowUnixPwBind, - Attribute::KeyActionRevoke, - Attribute::KeyActionRotate, - ], - ..Default::default() - }; -} - -lazy_static! { - pub static ref IDM_ACP_DOMAIN_ADMIN_DL8: BuiltinAcp = BuiltinAcp { - classes: vec![ - EntryClass::Object, - EntryClass::AccessControlProfile, - EntryClass::AccessControlModify, - EntryClass::AccessControlSearch - ], - name: "idm_acp_domain_admin", - uuid: UUID_IDM_ACP_DOMAIN_ADMIN_V1, - description: "Builtin IDM Control for granting domain info administration locally", - receiver: BuiltinAcpReceiver::Group(vec![UUID_DOMAIN_ADMINS]), - target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![ - ProtoFilter::Eq( - Attribute::Uuid.to_string(), - STR_UUID_DOMAIN_INFO.to_string() - ), - FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone() - ])), - search_attrs: vec![ - Attribute::Class, - Attribute::Name, - Attribute::Uuid, - Attribute::DomainDisplayName, - Attribute::DomainName, - Attribute::DomainLdapBasedn, - Attribute::LdapMaxQueryableAttrs, - Attribute::DomainSsid, - Attribute::DomainUuid, - Attribute::KeyInternalData, - Attribute::LdapAllowUnixPwBind, - Attribute::Version, - Attribute::Image, - ], - modify_removed_attrs: vec![ - Attribute::DomainDisplayName, - Attribute::DomainSsid, - Attribute::DomainLdapBasedn, - Attribute::LdapMaxQueryableAttrs, - Attribute::LdapAllowUnixPwBind, - Attribute::KeyActionRevoke, - Attribute::KeyActionRotate, - Attribute::Image, - ], - modify_present_attrs: vec![ - Attribute::DomainDisplayName, - Attribute::DomainLdapBasedn, - Attribute::LdapMaxQueryableAttrs, - Attribute::DomainSsid, - Attribute::LdapAllowUnixPwBind, - Attribute::KeyActionRevoke, - Attribute::KeyActionRotate, - Attribute::Image, - ], - ..Default::default() - }; -} - lazy_static! { pub static ref IDM_ACP_DOMAIN_ADMIN_DL9: BuiltinAcp = BuiltinAcp { classes: vec![ diff --git a/server/lib/src/migration_data/dl10/mod.rs b/server/lib/src/migration_data/dl10/mod.rs index 8eac91720..483493b5b 100644 --- a/server/lib/src/migration_data/dl10/mod.rs +++ b/server/lib/src/migration_data/dl10/mod.rs @@ -105,6 +105,7 @@ pub fn phase_1_schema_attrs() -> Vec<EntryInitNew> { // DL10 SCHEMA_ATTR_DENIED_NAME_DL10.clone().into(), SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES.clone().into(), + SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_RS256_DL6.clone().into(), ] } @@ -140,6 +141,7 @@ pub fn phase_2_schema_classes() -> Vec<EntryInitNew> { SCHEMA_CLASS_OAUTH2_RS_DL9.clone().into(), // DL10 SCHEMA_CLASS_DOMAIN_INFO_DL10.clone().into(), + SCHEMA_CLASS_KEY_OBJECT_JWT_RS256.clone().into(), ] } @@ -260,8 +262,9 @@ pub fn phase_7_builtin_access_control_profiles() -> Vec<EntryInitNew> { IDM_ACP_MAIL_SERVERS_DL8.clone().into(), IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL8.clone().into(), // DL9 - IDM_ACP_OAUTH2_MANAGE_DL9.clone().into(), IDM_ACP_GROUP_MANAGE_DL9.clone().into(), IDM_ACP_DOMAIN_ADMIN_DL9.clone().into(), + // DL10 + IDM_ACP_OAUTH2_MANAGE.clone().into(), ] } diff --git a/server/lib/src/migration_data/dl10/schema.rs b/server/lib/src/migration_data/dl10/schema.rs index 5117f7121..ec562d6db 100644 --- a/server/lib/src/migration_data/dl10/schema.rs +++ b/server/lib/src/migration_data/dl10/schema.rs @@ -657,6 +657,17 @@ pub static ref SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256_DL6: SchemaAttribute = Sc ..Default::default() }; +pub static ref SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_RS256_DL6: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_RS256, + name: Attribute::KeyActionImportJwsRs256, + description: "".to_string(), + multivalue: true, + // Ephemeral action. + phantom: true, + syntax: SyntaxType::PrivateBinary, + ..Default::default() +}; + pub static ref SCHEMA_ATTR_PATCH_LEVEL_DL7: SchemaAttribute = SchemaAttribute { uuid: UUID_SCHEMA_ATTR_PATCH_LEVEL, name: Attribute::PatchLevel, @@ -948,13 +959,12 @@ pub static ref SCHEMA_CLASS_SYSTEM_CONFIG: SchemaClass = SchemaClass { pub static ref SCHEMA_CLASS_OAUTH2_RS_DL9: SchemaClass = SchemaClass { uuid: UUID_SCHEMA_CLASS_OAUTH2_RS, name: EntryClass::OAuth2ResourceServer.into(), - description: "The class representing a configured OAuth2 Client".to_string(), + description: "The class epresenting a configured OAuth2 Client".to_string(), systemmay: vec![ Attribute::Description, Attribute::OAuth2RsScopeMap, Attribute::OAuth2RsSupScopeMap, - Attribute::Rs256PrivateKeyDer, Attribute::OAuth2JwtLegacyCryptoEnable, Attribute::OAuth2PreferShortUsername, Attribute::Image, @@ -963,11 +973,13 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_DL9: SchemaClass = SchemaClass { Attribute::OAuth2RsOrigin, Attribute::OAuth2StrictRedirectUri, Attribute::OAuth2DeviceFlowEnable, + // Deprecated + Attribute::Rs256PrivateKeyDer, + Attribute::OAuth2RsTokenKey, + Attribute::Es256PrivateKeyDer, ], systemmust: vec![ Attribute::OAuth2RsOriginLanding, - Attribute::OAuth2RsTokenKey, - Attribute::Es256PrivateKeyDer, ], ..Default::default() }; @@ -1045,6 +1057,16 @@ pub static ref SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6: SchemaClass = SchemaClass ..Default::default() }; +pub static ref SCHEMA_CLASS_KEY_OBJECT_JWT_RS256: SchemaClass = SchemaClass { + uuid: UUID_SCHEMA_CLASS_KEY_OBJECT_JWT_RS256, + name: EntryClass::KeyObjectJwtRs256.into(), + description: "A marker class indicating that this keyobject must provide jwt rs256 capability.".to_string(), + systemsupplements: vec![ + EntryClass::KeyObject.into(), + ], + ..Default::default() +}; + pub static ref SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6: SchemaClass = SchemaClass { uuid: UUID_SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM, name: EntryClass::KeyObjectJweA128GCM.into(), diff --git a/server/lib/src/plugins/keyobject.rs b/server/lib/src/plugins/keyobject.rs index b24f8cbff..d8ba04941 100644 --- a/server/lib/src/plugins/keyobject.rs +++ b/server/lib/src/plugins/keyobject.rs @@ -135,11 +135,13 @@ impl KeyObjectManagement { qs: &mut QueryServerWriteTransaction, cand: &mut [Entry<EntryInvalid, T>], ) -> Result<(), OperationError> { - // Valid from right meow! + // New keys will be valid from right meow! let valid_from = qs.get_curtime(); let txn_cid = qs.get_cid().clone(); - let key_providers = qs.get_key_providers_mut(); + // ==================================================================== + // Transform any found KeyObjects and manage any related key operations + // for them cand.iter_mut() .filter(|entry| { @@ -172,6 +174,14 @@ impl KeyObjectManagement { key_object.jws_es256_import(import_keys, valid_from, &txn_cid)?; } + let maybe_import = entry.pop_ava(Attribute::KeyActionImportJwsRs256); + if let Some(import_keys) = maybe_import + .as_ref() + .and_then(|vs| vs.as_private_binary_set()) + { + key_object.jws_rs256_import(import_keys, valid_from, &txn_cid)?; + } + // If revoke. This weird looking let dance is to ensure that the inner hexstring set // lives long enough. let maybe_revoked = entry.pop_ava(Attribute::KeyActionRevoke); @@ -208,6 +218,11 @@ impl KeyObjectManagement { key_object.jws_es256_assert(Duration::ZERO, &txn_cid)?; } + if entry.attribute_equality(Attribute::Class, &EntryClass::KeyObjectJwtRs256.into()) + { + key_object.jws_rs256_assert(Duration::ZERO, &txn_cid)?; + } + if entry .attribute_equality(Attribute::Class, &EntryClass::KeyObjectJweA128GCM.into()) { diff --git a/server/lib/src/plugins/mod.rs b/server/lib/src/plugins/mod.rs index 87668467c..a9d00c2f8 100644 --- a/server/lib/src/plugins/mod.rs +++ b/server/lib/src/plugins/mod.rs @@ -18,10 +18,10 @@ mod domain; pub(crate) mod dyngroup; mod eckeygen; pub(crate) mod gidnumber; -mod jwskeygen; mod keyobject; mod memberof; mod namehistory; +mod oauth2; mod refint; mod session; mod spn; @@ -230,15 +230,17 @@ impl Plugins { ) -> Result<(), OperationError> { base::Base::pre_create_transform(qs, cand, ce)?; valuedeny::ValueDeny::pre_create_transform(qs, cand, ce)?; - cred_import::CredImport::pre_create_transform(qs, cand, ce)?; + + oauth2::OAuth2::pre_create_transform(qs, cand, ce)?; + eckeygen::EcdhKeyGen::pre_create_transform(qs, cand, ce)?; keyobject::KeyObjectManagement::pre_create_transform(qs, cand, ce)?; - jwskeygen::JwsKeygen::pre_create_transform(qs, cand, ce)?; + cred_import::CredImport::pre_create_transform(qs, cand, ce)?; + gidnumber::GidNumber::pre_create_transform(qs, cand, ce)?; domain::Domain::pre_create_transform(qs, cand, ce)?; spn::Spn::pre_create_transform(qs, cand, ce)?; default_values::DefaultValues::pre_create_transform(qs, cand, ce)?; namehistory::NameHistory::pre_create_transform(qs, cand, ce)?; - eckeygen::EcdhKeyGen::pre_create_transform(qs, cand, ce)?; // Should always be last attrunique::AttrUnique::pre_create_transform(qs, cand, ce) } @@ -271,16 +273,18 @@ impl Plugins { ) -> Result<(), OperationError> { base::Base::pre_modify(qs, pre_cand, cand, me)?; valuedeny::ValueDeny::pre_modify(qs, pre_cand, cand, me)?; - cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?; - jwskeygen::JwsKeygen::pre_modify(qs, pre_cand, cand, me)?; + + oauth2::OAuth2::pre_modify(qs, pre_cand, cand, me)?; + eckeygen::EcdhKeyGen::pre_modify(qs, pre_cand, cand, me)?; keyobject::KeyObjectManagement::pre_modify(qs, pre_cand, cand, me)?; + cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?; + gidnumber::GidNumber::pre_modify(qs, pre_cand, cand, me)?; domain::Domain::pre_modify(qs, pre_cand, cand, me)?; spn::Spn::pre_modify(qs, pre_cand, cand, me)?; session::SessionConsistency::pre_modify(qs, pre_cand, cand, me)?; default_values::DefaultValues::pre_modify(qs, pre_cand, cand, me)?; namehistory::NameHistory::pre_modify(qs, pre_cand, cand, me)?; - eckeygen::EcdhKeyGen::pre_modify(qs, pre_cand, cand, me)?; // attr unique should always be last attrunique::AttrUnique::pre_modify(qs, pre_cand, cand, me) } @@ -306,16 +310,18 @@ impl Plugins { ) -> Result<(), OperationError> { base::Base::pre_batch_modify(qs, pre_cand, cand, me)?; valuedeny::ValueDeny::pre_batch_modify(qs, pre_cand, cand, me)?; - cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?; - jwskeygen::JwsKeygen::pre_batch_modify(qs, pre_cand, cand, me)?; + + oauth2::OAuth2::pre_batch_modify(qs, pre_cand, cand, me)?; + eckeygen::EcdhKeyGen::pre_batch_modify(qs, pre_cand, cand, me)?; keyobject::KeyObjectManagement::pre_batch_modify(qs, pre_cand, cand, me)?; + cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?; + gidnumber::GidNumber::pre_batch_modify(qs, pre_cand, cand, me)?; domain::Domain::pre_batch_modify(qs, pre_cand, cand, me)?; spn::Spn::pre_batch_modify(qs, pre_cand, cand, me)?; session::SessionConsistency::pre_batch_modify(qs, pre_cand, cand, me)?; default_values::DefaultValues::pre_batch_modify(qs, pre_cand, cand, me)?; namehistory::NameHistory::pre_batch_modify(qs, pre_cand, cand, me)?; - eckeygen::EcdhKeyGen::pre_batch_modify(qs, pre_cand, cand, me)?; // attr unique should always be last attrunique::AttrUnique::pre_batch_modify(qs, pre_cand, cand, me) } diff --git a/server/lib/src/plugins/jwskeygen.rs b/server/lib/src/plugins/oauth2.rs similarity index 59% rename from server/lib/src/plugins/jwskeygen.rs rename to server/lib/src/plugins/oauth2.rs index 2221eef35..e297772e8 100644 --- a/server/lib/src/plugins/jwskeygen.rs +++ b/server/lib/src/plugins/oauth2.rs @@ -6,14 +6,14 @@ use crate::plugins::Plugin; use crate::prelude::*; use crate::utils::password_from_random; -pub struct JwsKeygen {} +pub struct OAuth2 {} -impl Plugin for JwsKeygen { +impl Plugin for OAuth2 { fn id() -> &'static str { - "plugin_jws_keygen" + "plugin_oauth2" } - #[instrument(level = "debug", name = "jwskeygen_pre_create_transform", skip_all)] + #[instrument(level = "debug", name = "oauth2_pre_create_transform", skip_all)] fn pre_create_transform( qs: &mut QueryServerWriteTransaction, cand: &mut Vec<Entry<EntryInvalid, EntryNew>>, @@ -22,7 +22,7 @@ impl Plugin for JwsKeygen { Self::modify_inner(qs, cand) } - #[instrument(level = "debug", name = "jwskeygen_pre_modify", skip_all)] + #[instrument(level = "debug", name = "oauth2_pre_modify", skip_all)] fn pre_modify( qs: &mut QueryServerWriteTransaction, _pre_cand: &[Arc<EntrySealedCommitted>], @@ -32,7 +32,7 @@ impl Plugin for JwsKeygen { Self::modify_inner(qs, cand) } - #[instrument(level = "debug", name = "jwskeygen_pre_batch_modify", skip_all)] + #[instrument(level = "debug", name = "oauth2_pre_batch_modify", skip_all)] fn pre_batch_modify( qs: &mut QueryServerWriteTransaction, _pre_cand: &[Arc<EntrySealedCommitted>], @@ -43,52 +43,69 @@ impl Plugin for JwsKeygen { } } -impl JwsKeygen { +impl OAuth2 { fn modify_inner<T: Clone>( - _qs: &mut QueryServerWriteTransaction, + qs: &mut QueryServerWriteTransaction, cand: &mut [Entry<EntryInvalid, T>], ) -> Result<(), OperationError> { - cand.iter_mut().try_for_each(|e| { - if e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerBasic.into()) && - !e.attribute_pres(Attribute::OAuth2RsBasicSecret) { - security_info!("regenerating oauth2 basic secret"); - let v = Value::SecretValue(password_from_random()); - e.add_ava(Attribute::OAuth2RsBasicSecret, v); - } + let domain_level = qs.get_domain_version(); - if e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into()) { - if !e.attribute_pres(Attribute::OAuth2RsTokenKey) { - security_info!("regenerating oauth2 token key"); - let k = fernet::Fernet::generate_key(); - let v = Value::new_secret_str(&k); - e.add_ava(Attribute::OAuth2RsTokenKey, v); - } - if !e.attribute_pres(Attribute::Es256PrivateKeyDer) { - security_info!("regenerating oauth2 es256 private key"); - let der = JwsEs256Signer::generate_es256() - .and_then(|jws| jws.private_key_to_der()) - .map_err(|e| { - admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key"); - OperationError::CryptographyError - })?; - let v = Value::new_privatebinary(&der); - e.add_ava(Attribute::Es256PrivateKeyDer, v); - } - if e.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false) - && !e.attribute_pres(Attribute::Rs256PrivateKeyDer) { - security_info!("regenerating oauth2 legacy rs256 private key"); - let der = JwsRs256Signer::generate_legacy_rs256() - .and_then(|jws| jws.private_key_to_der()) - .map_err(|e| { - admin_error!(err = ?e, "Unable to generate Legacy RS256 JwsSigner private key"); - OperationError::CryptographyError - })?; - let v = Value::new_privatebinary(&der); - e.add_ava(Attribute::Rs256PrivateKeyDer, v); - } - } + cand.iter_mut() + .filter(|entry| { + entry.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into()) + }) + .try_for_each(|entry| { + // Regenerate the basic secret, if needed + if entry.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerBasic.into()) && + !entry.attribute_pres(Attribute::OAuth2RsBasicSecret) { + security_info!("regenerating oauth2 basic secret"); + let v = Value::SecretValue(password_from_random()); + entry.add_ava(Attribute::OAuth2RsBasicSecret, v); + } - Ok(()) + let has_rs256 = entry.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false); + + if domain_level >= DOMAIN_LEVEL_10 { + debug!("Generating OAuth2 Key Object"); + // OAuth2 now requires a KeyObject, configure it now. + entry.add_ava(Attribute::Class, EntryClass::KeyObject.to_value()); + entry.add_ava(Attribute::Class, EntryClass::KeyObjectJwtEs256.to_value()); + entry.add_ava(Attribute::Class, EntryClass::KeyObjectJweA128GCM.to_value()); + if has_rs256 { + entry.add_ava(Attribute::Class, EntryClass::KeyObjectJwtRs256.to_value()); + } + } else { + if !entry.attribute_pres(Attribute::OAuth2RsTokenKey) { + security_info!("regenerating oauth2 token key"); + let k = password_from_random(); + let v = Value::new_secret_str(&k); + entry.add_ava(Attribute::OAuth2RsTokenKey, v); + } + if !entry.attribute_pres(Attribute::Es256PrivateKeyDer) { + security_info!("regenerating oauth2 es256 private key"); + let der = JwsEs256Signer::generate_es256() + .and_then(|jws| jws.private_key_to_der()) + .map_err(|e| { + admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key"); + OperationError::CryptographyError + })?; + let v = Value::new_privatebinary(&der); + entry.add_ava(Attribute::Es256PrivateKeyDer, v); + } + if has_rs256 && !entry.attribute_pres(Attribute::Rs256PrivateKeyDer) { + security_info!("regenerating oauth2 legacy rs256 private key"); + let der = JwsRs256Signer::generate_legacy_rs256() + .and_then(|jws| jws.private_key_to_der()) + .map_err(|e| { + admin_error!(err = ?e, "Unable to generate Legacy RS256 JwsSigner private key"); + OperationError::CryptographyError + })?; + let v = Value::new_privatebinary(&der); + entry.add_ava(Attribute::Rs256PrivateKeyDer, v); + } + } + + Ok(()) }) } } @@ -145,7 +162,6 @@ mod tests { .internal_search_uuid(uuid) .expect("failed to get oauth2 config"); assert!(e.attribute_pres(Attribute::OAuth2RsBasicSecret)); - assert!(e.attribute_pres(Attribute::OAuth2RsTokenKey)); } ); } @@ -186,8 +202,7 @@ mod tests { ( Attribute::OAuth2RsBasicSecret, Value::new_secret_str("12345") - ), - (Attribute::OAuth2RsTokenKey, Value::new_secret_str("12345")) + ) ); let preload = vec![e]; @@ -207,10 +222,8 @@ mod tests { .internal_search_uuid(uuid) .expect("failed to get oauth2 config"); assert!(e.attribute_pres(Attribute::OAuth2RsBasicSecret)); - assert!(e.attribute_pres(Attribute::OAuth2RsTokenKey)); // Check the values are different. assert!(e.get_ava_single_secret(Attribute::OAuth2RsBasicSecret) != Some("12345")); - assert!(e.get_ava_single_secret(Attribute::OAuth2RsTokenKey) != Some("12345")); } ); } diff --git a/server/lib/src/server/keys/internal.rs b/server/lib/src/server/keys/internal.rs index e925f64f0..2e265d719 100644 --- a/server/lib/src/server/keys/internal.rs +++ b/server/lib/src/server/keys/internal.rs @@ -1,24 +1,22 @@ use super::object::{KeyObject, KeyObjectT}; use super::KeyId; use crate::prelude::*; - -use smolset::SmolSet; - -use std::collections::{BTreeMap, BTreeSet}; -use std::sync::Arc; - +use crate::value::{KeyStatus, KeyUsage}; +use crate::valueset::{KeyInternalData, ValueSetKeyInternal}; use compact_jwt::compact::{JweAlg, JweCompact, JweEnc}; -use compact_jwt::crypto::{JweA128GCMEncipher, JweA128KWEncipher}; +use compact_jwt::crypto::{ + JweA128GCMEncipher, JweA128KWEncipher, JwsRs256Signer, JwsRs256Verifier, +}; use compact_jwt::jwe::Jwe; use compact_jwt::traits::*; use compact_jwt::{ - JwaAlg, Jwk, Jws, JwsCompact, JwsEs256Signer, JwsEs256Verifier, JwsSigner, JwsSignerToVerifier, + JwaAlg, Jwk, JwkKeySet, Jws, JwsCompact, JwsEs256Signer, JwsEs256Verifier, JwsSigner, + JwsSignerToVerifier, }; - +use smolset::SmolSet; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::Bound::{Included, Unbounded}; - -use crate::value::{KeyStatus, KeyUsage}; -use crate::valueset::{KeyInternalData, ValueSetKeyInternal}; +use std::sync::Arc; pub struct KeyProviderInternal { uuid: Uuid, @@ -61,6 +59,7 @@ impl KeyProviderInternal { uuid, jws_es256: None, jwe_a128gcm: None, + jws_rs256: None, })) } @@ -73,6 +72,7 @@ impl KeyProviderInternal { debug!(?uuid, "Loading key object ..."); let mut jws_es256: Option<KeyObjectInternalJwtEs256> = None; + let mut jws_rs256: Option<KeyObjectInternalJwtRs256> = None; let mut jwe_a128gcm: Option<KeyObjectInternalJweA128GCM> = None; if let Some(key_internal_map) = entry @@ -104,6 +104,18 @@ impl KeyProviderInternal { *valid_from, )?; } + KeyUsage::JwsRs256 => { + let jws_rs256_ref = + jws_rs256.get_or_insert_with(KeyObjectInternalJwtRs256::default); + + jws_rs256_ref.load( + key_id, + *status, + status_cid.clone(), + der, + *valid_from, + )?; + } KeyUsage::JweA128GCM => { let jwe_a128gcm_ref = jwe_a128gcm.get_or_insert_with(KeyObjectInternalJweA128GCM::default); @@ -125,6 +137,7 @@ impl KeyProviderInternal { uuid, jws_es256, jwe_a128gcm, + jws_rs256, }))) } @@ -194,7 +207,7 @@ impl KeyObjectInternalJweA128GCM { fn assert_active(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> { if self.get_valid_cipher(valid_from).is_none() { // This means there is no active signing key, so we need to create one. - warn!("no active jwe a128gcm found, creating a new one ..."); + debug!("no active jwe a128gcm found, creating a new one ..."); self.new_active(valid_from, cid) } else { Ok(()) @@ -422,7 +435,7 @@ impl KeyObjectInternalJwtEs256 { fn assert_active(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> { if self.get_valid_signer(valid_from).is_none() { // This means there is no active signing key, so we need to create one. - warn!("no active jwt es256 found, creating a new one ..."); + debug!("no active jwt es256 found, creating a new one ..."); self.new_active(valid_from, cid) } else { Ok(()) @@ -437,8 +450,8 @@ impl KeyObjectInternalJwtEs256 { ) -> Result<(), OperationError> { let valid_from = valid_from.as_secs(); - for der in import_keys { - let signer = JwsEs256Signer::from_es256_der(der).map_err(|err| { + for private_der in import_keys { + let signer = JwsEs256Signer::from_es256_der(private_der).map_err(|err| { error!(?err, "Unable to load imported es256 DER signer"); OperationError::KP0028KeyObjectImportJwsEs256DerInvalid })?; @@ -451,22 +464,19 @@ impl KeyObjectInternalJwtEs256 { OperationError::KP0029KeyObjectSignerToVerifier })?; - let public_der = verifier.public_key_to_der().map_err(|jwt_error| { - error!(?jwt_error, "Unable to convert public key to DER"); - OperationError::KP0030KeyObjectPublicToDer - })?; - // We need to use the legacy KID for imported objects let kid = signer.get_legacy_kid().to_string(); debug!(?kid, "imported key"); + self.active.insert(valid_from, signer.clone()); + self.all.insert( kid, InternalJwtEs256 { valid_from, - status: InternalJwtEs256Status::Retained { + status: InternalJwtEs256Status::Valid { verifier, - public_der, + private_der: private_der.clone(), }, status_cid: cid.clone(), }, @@ -696,6 +706,25 @@ impl KeyObjectInternalJwtEs256 { } } + fn public_jwks(&self) -> JwkKeySet { + let keys = self + .all + .iter() + .filter_map(|(_, es256)| match &es256.status { + InternalJwtEs256Status::Valid { verifier, .. } + | InternalJwtEs256Status::Retained { verifier, .. } => verifier + .public_key_as_jwk() + .map_err(|err| { + error!(?err); + }) + .ok(), + InternalJwtEs256Status::Revoked { .. } => None, + }) + .collect::<Vec<_>>(); + + JwkKeySet { keys } + } + fn public_jwk(&self, key_id: &str) -> Result<Option<Jwk>, OperationError> { if let Some(key_to_check) = self.all.get(key_id) { match &key_to_check.status { @@ -728,11 +757,387 @@ impl KeyObjectInternalJwtEs256 { } } +#[derive(Clone)] +enum InternalJwtRs256Status { + Valid { + verifier: JwsRs256Verifier, + private_der: Vec<u8>, + }, + Retained { + verifier: JwsRs256Verifier, + public_der: Vec<u8>, + }, + Revoked { + untrusted_verifier: JwsRs256Verifier, + public_der: Vec<u8>, + }, +} + +#[derive(Clone)] +struct InternalJwtRs256 { + valid_from: u64, + status: InternalJwtRs256Status, + status_cid: Cid, +} + +#[derive(Default, Clone)] +struct KeyObjectInternalJwtRs256 { + // active signing keys are in a BTreeMap indexed by their valid_from + // time so that we can retrieve the active key. + // + // We don't need to worry about manipulating this at runtime, since any expiry + // event will cause the keyObject to reload, which will reflect to this map. + active: BTreeMap<u64, JwsRs256Signer>, + + // All keys are stored by their KeyId for fast lookup. Keys internally have a + // current status which is checked for signature validation. + all: BTreeMap<KeyId, InternalJwtRs256>, +} + +impl KeyObjectInternalJwtRs256 { + fn get_valid_signer(&self, time: Duration) -> Option<&JwsRs256Signer> { + let ct_secs = time.as_secs(); + + self.active + .range((Unbounded, Included(ct_secs))) + .next_back() + .map(|(_time, signer)| signer) + } + + fn assert_active(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> { + if self.get_valid_signer(valid_from).is_none() { + // This means there is no active signing key, so we need to create one. + debug!("no active jwt rs256 found, creating a new one ..."); + self.new_active(valid_from, cid) + } else { + Ok(()) + } + } + + fn import( + &mut self, + import_keys: &SmolSet<[Vec<u8>; 1]>, + valid_from: Duration, + cid: &Cid, + ) -> Result<(), OperationError> { + let valid_from = valid_from.as_secs(); + + for private_der in import_keys { + let signer = JwsRs256Signer::from_rs256_der(private_der).map_err(|err| { + error!(?err, "Unable to load imported rs256 DER signer"); + OperationError::KP0045KeyObjectImportJwsRs256DerInvalid + })?; + + let verifier = signer.get_verifier().map_err(|jwt_error| { + error!( + ?jwt_error, + "Unable to produce jwt rs256 verifier from signer" + ); + OperationError::KP0046KeyObjectSignerToVerifier + })?; + + // We need to use the legacy KID for imported objects + let kid = signer.get_legacy_kid().to_string(); + debug!(?kid, "imported key"); + + self.active.insert(valid_from, signer.clone()); + + self.all.insert( + kid, + InternalJwtRs256 { + valid_from, + status: InternalJwtRs256Status::Valid { + verifier, + private_der: private_der.clone(), + }, + status_cid: cid.clone(), + }, + ); + } + + Ok(()) + } + + fn new_active(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> { + let valid_from = valid_from.as_secs(); + + let signer = JwsRs256Signer::generate_legacy_rs256().map_err(|jwt_error| { + error!(?jwt_error, "Unable to generate new jwt rs256 signing key"); + OperationError::KP0048KeyObjectJwtRs256Generation + })?; + + let verifier = signer.get_verifier().map_err(|jwt_error| { + error!( + ?jwt_error, + "Unable to produce jwt rs256 verifier from signer" + ); + OperationError::KP0049KeyObjectSignerToVerifier + })?; + + let private_der = signer.private_key_to_der().map_err(|jwt_error| { + error!(?jwt_error, "Unable to convert signing key to DER"); + OperationError::KP0050KeyObjectPrivateToDer + })?; + + self.active.insert(valid_from, signer.clone()); + + let kid = signer.get_kid().to_string(); + + self.all.insert( + kid, + InternalJwtRs256 { + valid_from, + status: InternalJwtRs256Status::Valid { + // signer, + verifier, + private_der, + }, + status_cid: cid.clone(), + }, + ); + + Ok(()) + } + + fn revoke(&mut self, revoke_key_id: &KeyId, cid: &Cid) -> Result<bool, OperationError> { + if let Some(key_to_revoke) = self.all.get_mut(revoke_key_id) { + let untrusted_verifier = match &key_to_revoke.status { + InternalJwtRs256Status::Valid { verifier, .. } + | InternalJwtRs256Status::Retained { verifier, .. } => verifier, + InternalJwtRs256Status::Revoked { + untrusted_verifier, .. + } => untrusted_verifier, + } + .clone(); + + let public_der = untrusted_verifier + .public_key_to_der() + .map_err(|jwt_error| { + error!(?jwt_error, "Unable to convert public key to DER"); + OperationError::KP0051KeyObjectPublicToDer + })?; + + key_to_revoke.status = InternalJwtRs256Status::Revoked { + untrusted_verifier, + public_der, + }; + key_to_revoke.status_cid = cid.clone(); + + let valid_from = key_to_revoke.valid_from; + + // Remove it from the active set. + self.active.remove(&valid_from); + + Ok(true) + } else { + // We didn't revoke anything + Ok(false) + } + } + + fn load( + &mut self, + id: &str, + status: KeyStatus, + status_cid: Cid, + der: &[u8], + valid_from: u64, + ) -> Result<(), OperationError> { + let id: KeyId = id.to_string(); + + let status = match status { + KeyStatus::Valid => { + let signer = JwsRs256Signer::from_rs256_der(der).map_err(|err| { + error!(?err, ?id, "Unable to load rs256 DER signer"); + OperationError::KP0052KeyObjectJwsRs256DerInvalid + })?; + + let verifier = signer.get_verifier().map_err(|err| { + error!(?err, "Unable to retrieve verifier from signer"); + OperationError::KP0053KeyObjectSignerToVerifier + })?; + + self.active.insert(valid_from, signer); + + InternalJwtRs256Status::Valid { + // signer, + verifier, + private_der: der.to_vec(), + } + } + KeyStatus::Retained => { + let verifier = JwsRs256Verifier::from_rs256_der(der).map_err(|err| { + error!(?err, ?id, "Unable to load rs256 DER verifier"); + OperationError::KP0054KeyObjectJwsRs256DerInvalid + })?; + + InternalJwtRs256Status::Retained { + verifier, + public_der: der.to_vec(), + } + } + KeyStatus::Revoked => { + let untrusted_verifier = JwsRs256Verifier::from_rs256_der(der).map_err(|err| { + error!(?err, ?id, "Unable to load rs256 DER revoked verifier"); + OperationError::KP0055KeyObjectJwsRs256DerInvalid + })?; + + InternalJwtRs256Status::Revoked { + untrusted_verifier, + public_der: der.to_vec(), + } + } + }; + + let internal_jwt = InternalJwtRs256 { + valid_from, + status, + status_cid, + }; + + self.all.insert(id, internal_jwt); + + Ok(()) + } + + fn to_key_iter(&self) -> impl Iterator<Item = (KeyId, KeyInternalData)> + '_ { + self.all.iter().map(|(key_id, internal_jwt)| { + let usage = KeyUsage::JwsRs256; + + let valid_from = internal_jwt.valid_from; + let status_cid = internal_jwt.status_cid.clone(); + + let (status, der) = match &internal_jwt.status { + InternalJwtRs256Status::Valid { private_der, .. } => { + (KeyStatus::Valid, private_der.clone()) + } + InternalJwtRs256Status::Retained { public_der, .. } => { + (KeyStatus::Retained, public_der.clone()) + } + InternalJwtRs256Status::Revoked { public_der, .. } => { + (KeyStatus::Revoked, public_der.clone()) + } + }; + + ( + key_id.clone(), + KeyInternalData { + usage, + valid_from, + der, + status, + status_cid, + }, + ) + }) + } + + fn sign<V: JwsSignable>( + &self, + jws: &V, + current_time: Duration, + ) -> Result<V::Signed, OperationError> { + let Some(signing_key) = self.get_valid_signer(current_time) else { + error!("No signing keys available. This may indicate that no keys are valid yet!"); + return Err(OperationError::KP0061KeyObjectNoActiveSigningKeys); + }; + + signing_key.sign(jws).map_err(|jwt_err| { + error!(?jwt_err, "Unable to sign jws"); + OperationError::KP0056KeyObjectJwsRs256Signature + }) + } + + fn verify<V: JwsVerifiable>(&self, jwsc: &V) -> Result<V::Verified, OperationError> { + let internal_jws = jwsc + .kid() + .and_then(|kid| { + debug!(?kid); + self.all.get(kid) + }) + .ok_or_else(|| { + error!("JWS is signed by a key that is not present in this KeyObject"); + for pres_kid in self.all.keys() { + debug!(?pres_kid); + } + OperationError::KP0057KeyObjectJwsNotAssociated + })?; + + match &internal_jws.status { + InternalJwtRs256Status::Valid { verifier, .. } + | InternalJwtRs256Status::Retained { verifier, .. } => { + verifier.verify(jwsc).map_err(|jwt_err| { + error!(?jwt_err, "Failed to verify jws"); + OperationError::KP0058KeyObjectJwsInvalid + }) + } + InternalJwtRs256Status::Revoked { .. } => { + error!("The key used to sign this JWS has been revoked."); + Err(OperationError::KP0059KeyObjectJwsKeyRevoked) + } + } + } + + fn public_jwks(&self) -> JwkKeySet { + let keys = self + .all + .iter() + .filter_map(|(key_id, rs256)| { + error!(?key_id); + match &rs256.status { + InternalJwtRs256Status::Valid { verifier, .. } + | InternalJwtRs256Status::Retained { verifier, .. } => verifier + .public_key_as_jwk() + .map_err(|err| { + error!(?err); + }) + .ok(), + InternalJwtRs256Status::Revoked { .. } => None, + } + }) + .collect::<Vec<_>>(); + + JwkKeySet { keys } + } + + fn public_jwk(&self, key_id: &str) -> Result<Option<Jwk>, OperationError> { + if let Some(key_to_check) = self.all.get(key_id) { + match &key_to_check.status { + InternalJwtRs256Status::Valid { verifier, .. } + | InternalJwtRs256Status::Retained { verifier, .. } => { + verifier.public_key_as_jwk().map(Some).map_err(|err| { + error!(?err, "Unable to construct public JWK."); + OperationError::KP0060KeyObjectJwsPublicJwk + }) + } + InternalJwtRs256Status::Revoked { .. } => Ok(None), + } + } else { + Ok(None) + } + } + + #[cfg(test)] + fn kid_status(&self, key_id: &KeyId) -> Result<Option<KeyStatus>, OperationError> { + if let Some(key_to_check) = self.all.get(key_id) { + let status = match &key_to_check.status { + InternalJwtRs256Status::Valid { .. } => KeyStatus::Valid, + InternalJwtRs256Status::Retained { .. } => KeyStatus::Retained, + InternalJwtRs256Status::Revoked { .. } => KeyStatus::Revoked, + }; + Ok(Some(status)) + } else { + Ok(None) + } + } +} + #[derive(Clone)] pub struct KeyObjectInternal { provider: Arc<KeyProviderInternal>, uuid: Uuid, jws_es256: Option<KeyObjectInternalJwtEs256>, + jws_rs256: Option<KeyObjectInternalJwtRs256>, jwe_a128gcm: Option<KeyObjectInternalJweA128GCM>, // If you add more types here you need to add these to rotate // and revoke. @@ -769,6 +1174,10 @@ impl KeyObjectT for KeyObjectInternal { jws_es256_object.new_active(rotation_time, cid)?; } + if let Some(jws_rs256_object) = &mut self.jws_rs256 { + jws_rs256_object.new_active(rotation_time, cid)?; + } + if let Some(jwe_a128_gcm) = &mut self.jwe_a128gcm { jwe_a128_gcm.new_active(rotation_time, cid)?; } @@ -790,6 +1199,12 @@ impl KeyObjectT for KeyObjectInternal { } }; + if let Some(jws_rs256_object) = &mut self.jws_rs256 { + if jws_rs256_object.revoke(revoke_key_id, cid)? { + has_revoked = true; + } + }; + if let Some(jwe_a128_gcm) = &mut self.jwe_a128gcm { if jwe_a128_gcm.revoke(revoke_key_id, cid)? { has_revoked = true; @@ -831,6 +1246,14 @@ impl KeyObjectT for KeyObjectInternal { Err(OperationError::KP0018KeyProviderNoSuchKey) } } + JwaAlg::RS256 => { + if let Some(jws_rs256_object) = &self.jws_rs256 { + jws_rs256_object.verify(jwsc) + } else { + error!(provider_uuid = ?self.uuid, "jwt rs256 not available on this provider"); + Err(OperationError::KP0018KeyProviderNoSuchKey) + } + } unsupported_alg => { // unsupported rn. error!(provider_uuid = ?self.uuid, ?unsupported_alg, "algorithm not available on this provider"); @@ -839,12 +1262,26 @@ impl KeyObjectT for KeyObjectInternal { } } - fn jws_public_jwk(&self, kid: &str) -> Result<Option<Jwk>, OperationError> { + fn jws_es256_jwks(&self) -> Option<JwkKeySet> { + self.jws_es256 + .as_ref() + .map(|jws_es256_object| jws_es256_object.public_jwks()) + } + + fn jws_public_jwk(&self, key_id: &str) -> Result<Option<Jwk>, OperationError> { if let Some(jws_es256_object) = &self.jws_es256 { - jws_es256_object.public_jwk(kid) - } else { - Ok(None) + if let Some(status) = jws_es256_object.public_jwk(key_id)? { + return Ok(Some(status)); + } } + + if let Some(jws_rs256_object) = &self.jws_rs256 { + if let Some(status) = jws_rs256_object.public_jwk(key_id)? { + return Ok(Some(status)); + } + } + + Ok(None) } fn jws_es256_import( @@ -921,6 +1358,12 @@ impl KeyObjectT for KeyObjectInternal { } } + if let Some(jws_rs256_object) = &self.jws_rs256 { + if let Some(status) = jws_rs256_object.kid_status(key_id)? { + return Ok(Some(status)); + } + } + Ok(None) } @@ -933,6 +1376,11 @@ impl KeyObjectT for KeyObjectInternal { self.jwe_a128gcm .iter() .flat_map(|jwe_a128gcm| jwe_a128gcm.to_key_iter()), + ) + .chain( + self.jws_rs256 + .iter() + .flat_map(|jws_rs256| jws_rs256.to_key_iter()), ); let key_vs = ValueSetKeyInternal::from_key_iter(key_iter)? as ValueSet; @@ -948,6 +1396,46 @@ impl KeyObjectT for KeyObjectInternal { (Attribute::KeyInternalData, key_vs), ]) } + + fn jws_rs256_import( + &mut self, + import_keys: &SmolSet<[Vec<u8>; 1]>, + valid_from: Duration, + cid: &Cid, + ) -> Result<(), OperationError> { + let koi = self + .jws_rs256 + .get_or_insert_with(KeyObjectInternalJwtRs256::default); + + koi.import(import_keys, valid_from, cid) + } + + fn jws_rs256_assert(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError> { + let koi = self + .jws_rs256 + .get_or_insert_with(KeyObjectInternalJwtRs256::default); + + koi.assert_active(valid_from, cid) + } + + fn jws_rs256_sign( + &self, + jws: &Jws, + current_time: Duration, + ) -> Result<JwsCompact, OperationError> { + if let Some(jws_rs256_object) = &self.jws_rs256 { + jws_rs256_object.sign(jws, current_time) + } else { + error!(provider_uuid = ?self.uuid, "jwt rs256 not available on this provider"); + Err(OperationError::KP0062KeyProviderNoSuchKey) + } + } + + fn jws_rs256_jwks(&self) -> Option<JwkKeySet> { + self.jws_rs256 + .as_ref() + .map(|jws_rs256_object| jws_rs256_object.public_jwks()) + } } #[cfg(test)] diff --git a/server/lib/src/server/keys/object.rs b/server/lib/src/server/keys/object.rs index 7640a5cec..4fca79d61 100644 --- a/server/lib/src/server/keys/object.rs +++ b/server/lib/src/server/keys/object.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use compact_jwt::{compact::JweCompact, jwe::Jwe}; -use compact_jwt::{Jwk, Jws, JwsCompact}; +use compact_jwt::{Jwk, JwkKeySet, Jws, JwsCompact}; use smolset::SmolSet; use std::collections::BTreeSet; use uuid::Uuid; @@ -29,6 +29,25 @@ pub trait KeyObjectT { current_time: Duration, ) -> Result<JwsCompact, OperationError>; + fn jws_es256_jwks(&self) -> Option<JwkKeySet>; + + fn jws_rs256_import( + &mut self, + import_keys: &SmolSet<[Vec<u8>; 1]>, + valid_from: Duration, + cid: &Cid, + ) -> Result<(), OperationError>; + + fn jws_rs256_assert(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError>; + + fn jws_rs256_sign( + &self, + jws: &Jws, + current_time: Duration, + ) -> Result<JwsCompact, OperationError>; + + fn jws_rs256_jwks(&self) -> Option<JwkKeySet>; + fn jws_verify(&self, jwsc: &JwsCompact) -> Result<Jws, OperationError>; fn jws_public_jwk(&self, kid: &str) -> Result<Option<Jwk>, OperationError>; diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index b53afa579..fd0bca8db 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -532,6 +532,78 @@ impl QueryServerWriteTransaction<'_> { self.reload()?; + // =========== OAuth2 Cryptography Migration ============== + + debug!("START OAUTH2 MIGRATION"); + + // Load all the OAuth2 providers. + let all_oauth2_rs_entries = self.internal_search(filter!(f_eq( + Attribute::Class, + EntryClass::OAuth2ResourceServer.into() + )))?; + + if !all_oauth2_rs_entries.is_empty() { + let entry_iter = all_oauth2_rs_entries.iter().map(|tgt_entry| { + let entry_uuid = tgt_entry.get_uuid(); + let mut modlist = ModifyList::new_list(vec![ + Modify::Present(Attribute::Class, EntryClass::KeyObject.to_value()), + Modify::Present(Attribute::Class, EntryClass::KeyObjectJwtEs256.to_value()), + Modify::Present(Attribute::Class, EntryClass::KeyObjectJweA128GCM.to_value()), + // Delete the fernet key, rs256 if any, and the es256 key + Modify::Purged(Attribute::OAuth2RsTokenKey), + Modify::Purged(Attribute::Es256PrivateKeyDer), + Modify::Purged(Attribute::Rs256PrivateKeyDer), + ]); + + trace!(?tgt_entry); + + // Import the ES256 Key + if let Some(es256_private_der) = + tgt_entry.get_ava_single_private_binary(Attribute::Es256PrivateKeyDer) + { + modlist.push_mod(Modify::Present( + Attribute::KeyActionImportJwsEs256, + Value::PrivateBinary(es256_private_der.to_vec()), + )) + } else { + warn!("Unable to migrate es256 key"); + } + + let has_rs256 = tgt_entry + .get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable) + .unwrap_or(false); + + // If there is an rs256 key, import it. + // Import the RS256 Key + if has_rs256 { + modlist.push_mod(Modify::Present( + Attribute::Class, + EntryClass::KeyObjectJwtEs256.to_value(), + )); + + if let Some(rs256_private_der) = + tgt_entry.get_ava_single_private_binary(Attribute::Rs256PrivateKeyDer) + { + modlist.push_mod(Modify::Present( + Attribute::KeyActionImportJwsRs256, + Value::PrivateBinary(rs256_private_der.to_vec()), + )) + } else { + warn!("Unable to migrate rs256 key"); + } + } + + (entry_uuid, modlist) + }); + + self.internal_batch_modify(entry_iter)?; + } + + // Reload for new keys, and updated oauth2 + self.reload()?; + + // Done! + Ok(()) } diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs index fb3c27ca6..981b19718 100644 --- a/server/lib/src/value.rs +++ b/server/lib/src/value.rs @@ -1251,6 +1251,7 @@ pub struct Oauth2Session { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum KeyUsage { JwsEs256, + JwsRs256, JweA128GCM, } @@ -1261,6 +1262,7 @@ impl fmt::Display for KeyUsage { "{}", match self { KeyUsage::JwsEs256 => "jws_es256", + KeyUsage::JwsRs256 => "jws_rs256", KeyUsage::JweA128GCM => "jwe_a128gcm", } ) diff --git a/server/lib/src/valueset/key_internal.rs b/server/lib/src/valueset/key_internal.rs index fb5034595..e657238b9 100644 --- a/server/lib/src/valueset/key_internal.rs +++ b/server/lib/src/valueset/key_internal.rs @@ -83,6 +83,7 @@ impl ValueSetKeyInternal { let id: KeyId = id; let usage = match usage { DbValueKeyUsage::JwsEs256 => KeyUsage::JwsEs256, + DbValueKeyUsage::JwsRs256 => KeyUsage::JwsRs256, DbValueKeyUsage::JweA128GCM => KeyUsage::JweA128GCM, }; let status_cid = status_cid.into(); @@ -131,6 +132,7 @@ impl ValueSetKeyInternal { let id: String = id.clone(); let usage = match usage { KeyUsage::JwsEs256 => DbValueKeyUsage::JwsEs256, + KeyUsage::JwsRs256 => DbValueKeyUsage::JwsRs256, KeyUsage::JweA128GCM => DbValueKeyUsage::JweA128GCM, }; let status_cid = status_cid.into(); diff --git a/server/testkit/tests/testkit/oauth2_test.rs b/server/testkit/tests/testkit/oauth2_test.rs index 8de7dab73..be0290940 100644 --- a/server/testkit/tests/testkit/oauth2_test.rs +++ b/server/testkit/tests/testkit/oauth2_test.rs @@ -1,9 +1,6 @@ #![deny(warnings)] -use std::collections::{BTreeMap, BTreeSet}; -use std::convert::TryFrom; -use std::str::FromStr; - use compact_jwt::{JwkKeySet, JwsEs256Verifier, JwsVerifier, OidcToken, OidcUnverified}; +use kanidm_client::{http::header, KanidmClient, StatusCode}; use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT}; use kanidm_proto::constants::*; use kanidm_proto::internal::Oauth2ClaimMapJoin; @@ -14,18 +11,20 @@ use kanidm_proto::oauth2::{ }; use kanidmd_lib::constants::NAME_IDM_ALL_ACCOUNTS; use kanidmd_lib::prelude::Attribute; -use oauth2_ext::PkceCodeChallenge; -use reqwest::header::{HeaderValue, CONTENT_TYPE}; -use uri::{OAUTH2_TOKEN_ENDPOINT, OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT}; -use url::{form_urlencoded::parse as query_parse, Url}; - -use kanidm_client::{http::header, KanidmClient, StatusCode}; use kanidmd_testkit::{ assert_no_cache, ADMIN_TEST_PASSWORD, ADMIN_TEST_USER, NOT_ADMIN_TEST_EMAIL, NOT_ADMIN_TEST_PASSWORD, NOT_ADMIN_TEST_USERNAME, TEST_INTEGRATION_RS_DISPLAY, TEST_INTEGRATION_RS_GROUP_ALL, TEST_INTEGRATION_RS_ID, TEST_INTEGRATION_RS_REDIRECT_URL, TEST_INTEGRATION_RS_URL, }; +use oauth2_ext::PkceCodeChallenge; +use reqwest::header::{HeaderValue, CONTENT_TYPE}; +use std::collections::{BTreeMap, BTreeSet}; +use std::convert::TryFrom; +use std::str::FromStr; +use time::OffsetDateTime; +use uri::{OAUTH2_TOKEN_ENDPOINT, OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT}; +use url::{form_urlencoded::parse as query_parse, Url}; /// Tests an OAuth 2.0 / OpenID confidential client Authorisation Client flow. /// @@ -91,10 +90,15 @@ async fn test_oauth2_openid_basic_flow_impl( .expect("Failed to configure account password"); rsclient - .idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true, true, true) + .idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true) .await .expect("Failed to update oauth2 config"); + rsclient + .idm_oauth2_rs_rotate_keys(TEST_INTEGRATION_RS_ID, OffsetDateTime::now_utc()) + .await + .expect("Failed to rotate oauth2 keys"); + rsclient .idm_oauth2_rs_update_scope_map( TEST_INTEGRATION_RS_ID, @@ -621,7 +625,7 @@ async fn test_oauth2_openid_public_flow_impl( .expect("Failed to configure account password"); rsclient - .idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true, true, true) + .idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true) .await .expect("Failed to update oauth2 config"); diff --git a/server/testkit/tests/testkit/proto_v1_test.rs b/server/testkit/tests/testkit/proto_v1_test.rs index b55dfcd30..fdcf15638 100644 --- a/server/testkit/tests/testkit/proto_v1_test.rs +++ b/server/testkit/tests/testkit/proto_v1_test.rs @@ -1,9 +1,7 @@ #![deny(warnings)] -use std::path::Path; -use std::time::SystemTime; - +use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier}; +use kanidm_client::{ClientError, KanidmClient}; use kanidm_proto::constants::{ATTR_GIDNUMBER, KSESSIONID}; - use kanidm_proto::internal::{ ApiToken, CURegState, Filter, ImageValue, Modify, ModifyList, UatPurpose, UserAuthToken, }; @@ -13,19 +11,16 @@ use kanidm_proto::v1::{ }; use kanidmd_lib::constants::{NAME_IDM_ADMINS, NAME_SYSTEM_ADMINS}; use kanidmd_lib::credential::totp::Totp; - use kanidmd_lib::prelude::Attribute; -use tracing::{debug, trace}; - +use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER}; +use std::path::Path; use std::str::FromStr; - -use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier}; +use std::time::SystemTime; +use time::OffsetDateTime; +use tracing::{debug, trace}; use webauthn_authenticator_rs::softpasskey::SoftPasskey; use webauthn_authenticator_rs::WebauthnAuthenticator; -use kanidm_client::{ClientError, KanidmClient}; -use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER}; - const UNIX_TEST_PASSWORD: &str = "unix test user password"; #[kanidmd_testkit::test] @@ -851,8 +846,7 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: &KanidmClient) { assert_eq!(initial_configs.len(), 1); - // Get the value. Assert we have oauth2_rs_basic_secret, - // but can NOT see the token_secret. + // Get the value. Assert we have oauth2_rs_basic_secret. let oauth2_config = rsclient .idm_oauth2_rs_get("test_integration") .await @@ -866,10 +860,6 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: &KanidmClient) { assert!(oauth2_config .attrs .contains_key(Attribute::OAuth2RsBasicSecret.as_str())); - // This is present, but redacted. - assert!(oauth2_config - .attrs - .contains_key(Attribute::OAuth2RsTokenKey.as_str())); // Mod delete the secret/key and check them again. // Check we can patch the oauth2_rs_name / oauth2_rs_origin @@ -880,12 +870,15 @@ async fn test_server_rest_oauth2_basic_lifecycle(rsclient: &KanidmClient) { Some("Test Integration"), Some("https://new_demo.example.com"), true, - true, - true, ) .await .expect("Failed to update config"); + rsclient + .idm_oauth2_rs_rotate_keys("test_integration", OffsetDateTime::now_utc()) + .await + .expect("Failed to rotate oauth2 keys"); + let oauth2_config_updated = rsclient .idm_oauth2_rs_get("test_integration") .await diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index fdb9e2dfd..7832582eb 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -73,6 +73,7 @@ serde = { workspace = true } serde_json = { workspace = true } uuid = { workspace = true } url = { workspace = true } +time = { workspace = true } ## See src/cli/webauthn/mod.rs for which features are ## required for which target_os here diff --git a/tools/cli/src/cli/oauth2.rs b/tools/cli/src/cli/oauth2.rs index d1064efe3..3f6a2383f 100644 --- a/tools/cli/src/cli/oauth2.rs +++ b/tools/cli/src/cli/oauth2.rs @@ -1,12 +1,11 @@ use crate::common::OpType; +use crate::Oauth2ClaimMapJoin; use crate::{handle_client_error, Oauth2Opt, OutputMode}; use anyhow::{Context, Error}; +use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin as ProtoOauth2ClaimMapJoin}; use std::fs::read; use std::process::exit; -use crate::Oauth2ClaimMapJoin; -use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin as ProtoOauth2ClaimMapJoin}; - impl Oauth2Opt { pub fn debug(&self) -> bool { match self { @@ -47,7 +46,9 @@ impl Oauth2Opt { | Oauth2Opt::EnableStrictRedirectUri { copt, .. } | Oauth2Opt::DisableStrictRedirectUri { copt, .. } | Oauth2Opt::AddOrigin { copt, .. } - | Oauth2Opt::RemoveOrigin { copt, .. } => copt.debug, + | Oauth2Opt::RemoveOrigin { copt, .. } + | Oauth2Opt::RotateCryptographicKeys { copt, .. } + | Oauth2Opt::RevokeCryptographicKey { copt, .. } => copt.debug, } } @@ -196,7 +197,7 @@ impl Oauth2Opt { Oauth2Opt::ResetSecrets(cbopt) => { let client = cbopt.copt.to_client(OpType::Write).await; match client - .idm_oauth2_rs_update(cbopt.name.as_str(), None, None, None, true, true, true) + .idm_oauth2_rs_update(cbopt.name.as_str(), None, None, None, true) .await { Ok(_) => println!("Success"), @@ -238,8 +239,6 @@ impl Oauth2Opt { Some(cbopt.displayname.as_str()), None, false, - false, - false, ) .await { @@ -256,8 +255,6 @@ impl Oauth2Opt { None, None, false, - false, - false, ) .await { @@ -268,15 +265,7 @@ impl Oauth2Opt { Oauth2Opt::SetLandingUrl { nopt, url } => { let client = nopt.copt.to_client(OpType::Write).await; match client - .idm_oauth2_rs_update( - nopt.name.as_str(), - None, - None, - Some(url.as_str()), - false, - false, - false, - ) + .idm_oauth2_rs_update(nopt.name.as_str(), None, None, Some(url.as_str()), false) .await { Ok(_) => println!("Success"), @@ -522,6 +511,30 @@ impl Oauth2Opt { Err(e) => handle_client_error(e, copt.output_mode), } } + Oauth2Opt::RotateCryptographicKeys { + copt, + name, + rotate_at, + } => { + let client = copt.to_client(OpType::Write).await; + match client + .idm_oauth2_rs_rotate_keys(name.as_str(), *rotate_at) + .await + { + Ok(_) => println!("Success"), + Err(e) => handle_client_error(e, copt.output_mode), + } + } + Oauth2Opt::RevokeCryptographicKey { copt, name, key_id } => { + let client = copt.to_client(OpType::Write).await; + match client + .idm_oauth2_rs_revoke_key(name.as_str(), key_id.as_str()) + .await + { + Ok(_) => println!("Success"), + Err(e) => handle_client_error(e, copt.output_mode), + } + } } } } diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index 5e02a10ba..3a0f1386b 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -1,6 +1,12 @@ use clap::{builder::PossibleValue, Args, Subcommand, ValueEnum}; use kanidm_proto::internal::ImageType; use std::fmt; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; + +fn parse_rfc3339(input: &str) -> Result<OffsetDateTime, time::error::Parse > { + OffsetDateTime::parse(input, &Rfc3339) +} #[derive(Debug, Args)] pub struct Named { @@ -1131,8 +1137,9 @@ pub enum Oauth2Opt { group: String, }, - #[clap(name = "reset-secrets")] - /// Reset the secrets associated to this client + #[clap(name = "reset-basic-secret")] + /// Reset the client basic secret. You will need to update your client after + /// executing this. ResetSecrets(Named), #[clap(name = "show-basic-secret")] /// Show the associated basic secret for this client @@ -1143,7 +1150,7 @@ pub enum Oauth2Opt { /// Set a new display name for a client #[clap(name = "set-displayname")] SetDisplayname(Oauth2SetDisplayname), - /// Set a new name for this client. You may need to update + /// Set a new name for this client. You will need to update /// your integrated applications after this so that they continue to /// function correctly. #[clap(name = "set-name")] @@ -1256,6 +1263,28 @@ pub enum Oauth2Opt { #[cfg(feature = "dev-oauth2-device-flow")] /// Disable OAuth2 Device Flow authentication DeviceFlowDisable(Named), + /// Rotate the signing and encryption keys used by this client. The rotation + /// will occur at the specified time, or immediately if the time is in the past. + /// Past signatures will continue to operate even after a rotation occurs. If you + /// have concerns a key is compromised, then you should revoke it instead. + #[clap(name = "rotate-cryptographic-keys")] + RotateCryptographicKeys { + #[clap(flatten)] + copt: CommonOpt, + name: String, + #[clap(value_parser = parse_rfc3339)] + rotate_at: OffsetDateTime, + }, + /// Revoke the signing and encryption keys used by this client. This will immediately + /// trigger a rotation of the key in question, and signtatures or tokens issued by + /// the revoked key will not be considered valid. + #[clap(name = "revoke-cryptographic-key")] + RevokeCryptographicKey { + #[clap(flatten)] + copt: CommonOpt, + name: String, + key_id: String, + }, } #[derive(Args, Debug)]