From bd41ef8f911b7c86c855566e68a9fbdbe8f68e33 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Mon, 14 Mar 2022 17:29:04 +1000 Subject: [PATCH] Add design doc, revive the domain wide enc token (#649) * Add design doc, revive the domain wide enc token, use jwt from our lib instead of bundy, update docs --- Cargo.lock | 37 +++-- Cargo.toml | 2 + designs/credential-update.rst | 162 +++++++++++++++++++++ kanidm_book/src/installing_client_tools.md | 25 ++-- kanidm_book/src/pam_and_nsswitch.md | 11 +- kanidm_client/src/asynchronous.rs | 2 +- kanidm_tools/Cargo.toml | 2 +- kanidm_tools/src/cli/common.rs | 18 ++- kanidm_tools/src/cli/session.rs | 19 ++- kanidmd/Cargo.toml | 3 +- kanidmd/score/Cargo.toml | 3 +- kanidmd/score/src/https/mod.rs | 27 ++-- kanidmd/score/src/https/v1.rs | 51 +++++-- kanidmd/score/src/lib.rs | 7 +- kanidmd/src/lib/constants/acp.rs | 6 +- kanidmd/src/lib/constants/entries.rs | 2 +- kanidmd/src/lib/constants/schema.rs | 35 ++++- kanidmd/src/lib/constants/uuids.rs | 2 + kanidmd/src/lib/credential/mod.rs | 2 +- kanidmd/src/lib/entry.rs | 6 +- kanidmd/src/lib/idm/authsession.rs | 128 ++++++++-------- kanidmd/src/lib/idm/oauth2.rs | 4 + kanidmd/src/lib/idm/server.rs | 154 ++++++++++++++------ kanidmd/src/lib/plugins/domain.rs | 44 ++++-- kanidmd/src/lib/plugins/protected.rs | 18 ++- kanidmd/src/lib/server.rs | 36 ++++- kanidmd/src/lib/value.rs | 6 + 27 files changed, 607 insertions(+), 205 deletions(-) create mode 100644 designs/credential-update.rst diff --git a/Cargo.lock b/Cargo.lock index cba6ec716..7db29a771 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,20 +510,6 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" -[[package]] -name = "bundy" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d70ddb99e1c5a8308abe085b80c9ca05b41577aa73a2f431a5fec6837b5aa054" -dependencies = [ - "base64 0.13.0", - "log", - "openssl", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "byte-tools" version = "0.3.1" @@ -630,6 +616,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "compact_jwt" +version = "0.2.0" +source = "git+https://github.com/kanidm/compact-jwt.git#f093323d69dacd49189348cbb904d7c8a8f61cbd" +dependencies = [ + "base64 0.13.0", + "openssl", + "serde", + "serde_json", + "tracing", + "url", + "uuid", +] + [[package]] name = "compiled-uuid" version = "0.1.2" @@ -1887,9 +1887,8 @@ dependencies = [ "async-std", "async-trait", "base64 0.13.0", - "bundy", "chrono", - "compact_jwt", + "compact_jwt 0.2.0", "compiled-uuid", "concread", "criterion", @@ -1945,7 +1944,7 @@ version = "1.1.0-alpha.7" dependencies = [ "async-std", "base64 0.13.0", - "compact_jwt", + "compact_jwt 0.1.9", "futures", "kanidm", "kanidm_proto", @@ -1981,7 +1980,7 @@ dependencies = [ name = "kanidm_tools" version = "1.1.0-alpha.7" dependencies = [ - "bundy", + "compact_jwt 0.2.0", "dialoguer", "kanidm_client", "kanidm_proto", @@ -3181,7 +3180,7 @@ version = "0.1.0" dependencies = [ "async-std", "async-trait", - "bundy", + "compact_jwt 0.2.0", "futures-util", "kanidm", "kanidm_proto", diff --git a/Cargo.toml b/Cargo.toml index fa1e7c16a..652c0f244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,4 +41,6 @@ concread = { git = "https://github.com/kanidm/concread.git" } # webauthn-authenticator-rs = { git = "https://github.com/kanidm/webauthn-authenticator-rs.git", rev = "c91205e57a783a7c3a6e09ade377c83a86fe0900" } # compact_jwt = { path = "../compact_jwt" } +compact_jwt = { git = "https://github.com/kanidm/compact-jwt.git" } + diff --git a/designs/credential-update.rst b/designs/credential-update.rst new file mode 100644 index 000000000..db9a365c9 --- /dev/null +++ b/designs/credential-update.rst @@ -0,0 +1,162 @@ + +Credential Update and Onboarding Workflow +----------------------------------------- + +Letting users update their own credentials, but also allowing new users to create their own credentials +needs a new workflow. Since these are nearly identical, these processes can be combined. + +Major considerations are: + +* Ability to start the credential creation (onboarding) work flow from a secured link. +* Delegation of this permission from accounts to provide a reset path to users when needed. +* Ensure that any credential updates are consistent and atomic. +* Improved client side feedback around credential policy and rules. + +Initiation of the Credential Update Process +=========================================== + +The start of this process is that a user requests a credential update to begin for +themself, or on behalf of another user. + +Self Update Workflow +^^^^^^^^^^^^^^^^^^^^ + +The user signals intent to update their credentials. + +An ACP check is made to check that the user has self-write to the relevant credential fields. If they +do not, the credential update session is rejected. + +If they do have the access, a new credential update session is created, and the user is given a time +limited token allowing them to interact with the credential update session. + +If the credental update session is abandoned it can not be re-accessed until it is expired. + +Onboarding/Reset Account Workflow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The user signals intent that another users credentials should be updated. + +An ACP check is made to assert that the user has rights over the target users credential fields. +If they do not, the update session is rejected. Note that the target user does *NOT* need self +write permissions in this situation, which allows for a constrained permission on the target +users (IE anonymous has no self write). + +If the access exists, a intent token is created into a link which can be provided to the user. + +Exchange of this intent token, creates the time limited credential update session token. + +This allows the intent token to have a seperate time window, to the credential update session token. + +If the intent token creates a credential update session, and the credential update session is *not* +commited, it can be re-started by the intent token. + +If the credential update session has been committed, then the intent token can NOT create new +credential update sessions (it is once-use). + +Consistency +^^^^^^^^^^^ + +Only one credential update process may exist at a time for a user. Attempts to create a second +credential update session while an existing session exists is rejected until the previous session +is committed or timed out. + +The credential update session can be rejected by the user, canceling it's progress. + +A credential update session IS tied to a single server, similar to authentication. + +A credential update session has a unique-uuid assigned. When committed, this is added to a history-log +on the users account so they can see and audit the change. + +If an intent token is used, the uuid of the intent token becomes the uuid of the credential update session. + +If an intent token is exchanged, and it's uuid already exists in the history-log of the account, the +intent token is rejected. + +Credential Update Process +========================= + +The client on initiation of the credential update session is sent the policy related to the current +update session including: + +* The classes of valid credentials that *may* be created +* The current set of credentials that exist along with their metadata (private elements are NOT disclosed). + +The client then can build a set of changes to the set of credentials, expressing: + +* Modification of an existing credential. +* Creation of a new credential. +* Deletion of a credential. + +These changes are stored in the credential update session on the server. The reason for server side +assistance is that some classes of credentials require the server to be involved for the updates to function +and for consistent policy enforcement. + +Passwords may need to be sent to the server for checking against the badlist - since the badlist can +be very large, it is infeasible to send this to the client, so server assistance is required. + +Webauthn MUST use strong random challenges, and so the server MUST generate these to prevent +client side tampering. The server also MUST be involved in the attestation process. + +TOTP we must be able to detect SHA1 only authenticators. + +As a result, the built set of changes *is* persisted on the server in the credential update session +as the user interacts with and builds the set of changes. This allows the server to enforce that the update +session *must* represent a valid and complete set of compliant credentials before commit. + +The user may cancel the session at anytime, discarding any set of changes they had inflight. This allows +another session to now begin. + +If the user chooses to commit the changes, the server will assemble the changes into a modification +and apply it. The write is applied with server internal permissions - since we checked the permissions +during the create of the update session we can trust that the origin of this update has been validated. +Additionally since this is not an arbitrary write interface, this constrains potential risk. + +The update session MUST have an idle timeout, where a lack of interaction for an extended period causes +the session to invalidated. Interaction should extend the current time of the session up to a maximum window to +allow users to update their credentials without rush. + +A modification to a credential MUST change the UUID of the credential. This allows replication conflict ordering +to occur and create a linear and consistent timeline. + +A modification to a credential *should* offer a checkbox allowing users to invalidate sessions that were created +with that credential if they wish. + +A *deleted* credential *must* invalidate sessions that were created using that credential. + +Addition of Device Credentials +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +During a credential update session, a device credential may wish to be added. + +This should create a stub credential with a uuid. The client can then poll for updates to this +stub to determine when the device credential has been registered and it can display in the main user agent. + +A link is created that contains an encrypted device enrollment token. This contains the stub uuid +as well as the credential update session details. The device can partake in the enrollment process. + +The device enrollment token contains the relevant information related to the policy of the credential +so that the server that receives the token can enforce the credential adheres to this policy. + +If the client successfully enrolls, a new entry for the enrollment is created in the database. This +allows replication of the new credential to occur. + +The main session of the credential update can then check for the existance of this stub uuid in the +db and wait for it to replicate in. This can be checked by the "polling" action. + +When it has been replicated in, and polling has found the credential, the credentials are added to the session. The credential +can then have associated metadata altered (IE ident-only). + +During the commit, the stub credential object is DELETED. + +To prevent issues with DB size/growth, a stub credential reaper task MUST exist (similar to recycle/tombstone reaping). + + +Future Changes to ACP/Credentials +================================= + +Sudo Mode / Ident Only credentials + +These need flags in credentials, but we can add these later defaulting currently to the same which +is that all added credentials are sudo capable. + + diff --git a/kanidm_book/src/installing_client_tools.md b/kanidm_book/src/installing_client_tools.md index 8dbabc4b5..2df150aae 100644 --- a/kanidm_book/src/installing_client_tools.md +++ b/kanidm_book/src/installing_client_tools.md @@ -1,14 +1,15 @@ # Installing Client Tools -> **NOTE** As this project is in a rapid development phase, running different release versions will likely present incompatibilities. Ensure you're running the same release version of client/server binaries (eg. 1.1.0-alpha5, released 2021-07-07) +> **NOTE** As this project is in a rapid development phase, running different release versions will likely present incompatibilities. Ensure you're running matching release versions of client and server binaries. ## From packages Kanidm currently supports: * OpenSUSE Tumbleweed - * OpenSUSE Leap 15.3 - * Fedora 33/34 + * OpenSUSE Leap 15.3/15.4 + * Fedora 34/35 + * Centos Stream 9 ### OpenSUSE Tumbleweed @@ -18,9 +19,9 @@ the clients with: zypper ref zypper in kanidm-clients -### OpenSUSE Leap 15.3 +### OpenSUSE Leap 15.3/15.4 -Leap 15.3 is still not fully supported with Kanidm. For an experimental client, you can +Leap 15.3/15.4 is still not fully supported with Kanidm. For an experimental client, you can try the development repository. Using zypper you can add the repository with: zypper ar -f obs://network:idm network_idm @@ -30,15 +31,17 @@ Then you need to refresh your metadata and install the clients. zypper ref zypper in kanidm-clients -### Fedora +### Fedora / Centos Stream -Fedora is still experimentally supported through the development repository. You need to add the repository metadata into the correct directory. +Fedora has limited supported through the development repository. You need to add the repository metadata into the correct directory. cd /etc/yum.repos.d - # 33 - sudo wget https://download.opensuse.org/repositories/network:/idm/Fedora_33/network:idm.repo - # 34 + # Fedora 34 sudo wget https://download.opensuse.org/repositories/network:/idm/Fedora_34/network:idm.repo + # Fedora 35 + sudo wget https://download.opensuse.org/repositories/network:/idm/Fedora_35/network:idm.repo + # Centos Stream 9 + sudo wget https://download.opensuse.org/repositories/network:/idm/CentOS_9_Stream/network:idm.repo You can then install with: @@ -60,4 +63,4 @@ with the -C parameter: kanidm self whoami -C ../path/to/ca.pem -H https://localhost:8443 --name anonymous kanidm self whoami -H https://localhost:8443 --name anonymous -Now you can take some time to look at what commands are available - please [ask for help at any time](https://github.com/kanidm/kanidm#getting-in-contact--questions). \ No newline at end of file +Now you can take some time to look at what commands are available - please [ask for help at any time](https://github.com/kanidm/kanidm#getting-in-contact--questions). diff --git a/kanidm_book/src/pam_and_nsswitch.md b/kanidm_book/src/pam_and_nsswitch.md index 89255952f..53c168fe9 100644 --- a/kanidm_book/src/pam_and_nsswitch.md +++ b/kanidm_book/src/pam_and_nsswitch.md @@ -141,8 +141,10 @@ Each of these controls one of the four stages of PAM. The content should look li # /etc/pam.d/common-account-pc account [default=1 ignore=ignore success=ok] pam_localuser.so - account required pam_unix.so - account required pam_kanidm.so ignore_unknown_user + account sufficient pam_unix.so + account [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet_success quiet_fail + account sufficient pam_kanidm.so ignore_unknown_user + account pam_deny.so # /etc/pam.d/common-auth-pc auth required pam_env.so @@ -153,17 +155,18 @@ Each of these controls one of the four stages of PAM. The content should look li auth required pam_deny.so # /etc/pam.d/common-password-pc - password requisite pam_pwquality.so password [default=1 ignore=ignore success=ok] pam_localuser.so password required pam_unix.so use_authtok nullok shadow try_first_pass + password [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet_success quiet_fail password required pam_kanidm.so # /etc/pam.d/common-session-pc session optional pam_systemd.so session required pam_limits.so session optional pam_unix.so try_first_pass - session optional pam_kanidm.so session optional pam_umask.so + session [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet_success quiet_fail + session optional pam_kanidm.so session optional pam_env.so > **WARNING:** Ensure that `pam_mkhomedir` or `pam_oddjobd` are *not* present in your diff --git a/kanidm_client/src/asynchronous.rs b/kanidm_client/src/asynchronous.rs index 9163f5ee0..b2907cb5e 100644 --- a/kanidm_client/src/asynchronous.rs +++ b/kanidm_client/src/asynchronous.rs @@ -1363,7 +1363,7 @@ impl KanidmAsyncClient { } pub async fn idm_domain_reset_token_key(&self) -> Result<(), ClientError> { - self.perform_delete_request("/v1/domain/_attr/domain_token_key") + self.perform_delete_request("/v1/domain/_attr/es256_private_key_der") .await } diff --git a/kanidm_tools/Cargo.toml b/kanidm_tools/Cargo.toml index b4483ec70..74268af8c 100644 --- a/kanidm_tools/Cargo.toml +++ b/kanidm_tools/Cargo.toml @@ -41,7 +41,7 @@ shellexpand = "2.0" rayon = "1.2" time = { version = "0.2", features = ["serde", "std"] } qrcode = { version = "0.12", default-features = false } -bundy = "0.1" +compact_jwt = "^0.2.0" zxcvbn = "2.0" diff --git a/kanidm_tools/src/cli/common.rs b/kanidm_tools/src/cli/common.rs index b457ef0c3..cea3177b7 100644 --- a/kanidm_tools/src/cli/common.rs +++ b/kanidm_tools/src/cli/common.rs @@ -1,9 +1,10 @@ use crate::session::read_tokens; use crate::CommonOpt; +use compact_jwt::{Jws, JwsUnverified}; +use dialoguer::{theme::ColorfulTheme, Select}; use kanidm_client::{KanidmClient, KanidmClientBuilder}; use kanidm_proto::v1::UserAuthToken; - -use dialoguer::{theme::ColorfulTheme, Select}; +use std::str::FromStr; impl CommonOpt { pub fn to_unauth_client(&self) -> KanidmClient { @@ -91,8 +92,19 @@ impl CommonOpt { } }; + let jwtu = match JwsUnverified::from_str(&token) { + Ok(jwtu) => jwtu, + Err(e) => { + eprintln!("Unable to parse token - {:?}", e); + std::process::exit(1); + } + }; + // Is the token (probably) valid? - match unsafe { bundy::Data::parse_without_verification::(&token) } { + match jwtu + .validate_embeded() + .map(|jws: Jws| jws.inner) + { Ok(uat) => { if time::OffsetDateTime::now_utc() >= uat.expiry { error!( diff --git a/kanidm_tools/src/cli/session.rs b/kanidm_tools/src/cli/session.rs index d19ac557f..7cfdd927d 100644 --- a/kanidm_tools/src/cli/session.rs +++ b/kanidm_tools/src/cli/session.rs @@ -10,10 +10,13 @@ use std::fs::{create_dir, File}; use std::io::ErrorKind; use std::io::{self, BufReader, BufWriter, Write}; use std::path::PathBuf; +use std::str::FromStr; use webauthn_authenticator_rs::{u2fhid::U2FHid, RequestChallengeResponse, WebauthnAuthenticator}; use dialoguer::{theme::ColorfulTheme, Select}; +use compact_jwt::JwsUnverified; + static TOKEN_DIR: &str = "~/.cache"; static TOKEN_PATH: &str = "~/.cache/kanidm_tokens"; @@ -398,9 +401,21 @@ impl SessionOpt { }) .into_iter() .filter_map(|(u, t)| { - unsafe { bundy::Data::parse_without_verification::(&t) } + let jwtu = JwsUnverified::from_str(&t) + .map_err(|e| { + error!(?e, "Unable to parse token from str"); + }) + .ok()?; + + jwtu.validate_embeded() + .map_err(|e| { + error!(?e, "Unable to verify token signature, may be corrupt"); + }) + .map(|jwt| { + let uat = jwt.inner; + (u, (t, uat)) + }) .ok() - .map(|uat| (u, (t, uat))) }) .collect() } diff --git a/kanidmd/Cargo.toml b/kanidmd/Cargo.toml index 573f62fd6..71292939d 100644 --- a/kanidmd/Cargo.toml +++ b/kanidmd/Cargo.toml @@ -27,8 +27,7 @@ tide = "0.16" async-trait = "0.1" async-h1 = "2.0" fernet = { version = "^0.1.4", features = ["fernet_danger_timestamps"] } -bundy = "^0.1.1" -compact_jwt = "^0.1.7" +compact_jwt = "^0.2.0" async-std = { version = "1.6", features = ["tokio1"] } diff --git a/kanidmd/score/Cargo.toml b/kanidmd/score/Cargo.toml index 54d3e49a5..07a47452b 100644 --- a/kanidmd/score/Cargo.toml +++ b/kanidmd/score/Cargo.toml @@ -28,12 +28,11 @@ tokio-openssl = "0.6" openssl = "0.10" ldap3_proto = "0.2.2" -bundy = "^0.1.1" - tracing = { version = "0.1", features = ["attributes"] } serde = { version = "1", features = ["derive"] } async-trait = "0.1" async-std = { version = "1.6", features = ["tokio1"] } +compact_jwt = "^0.2.0" [build-dependencies] profiles = { path = "../../profiles" } diff --git a/kanidmd/score/src/https/mod.rs b/kanidmd/score/src/https/mod.rs index 82f42f377..f0a0eed1c 100644 --- a/kanidmd/score/src/https/mod.rs +++ b/kanidmd/score/src/https/mod.rs @@ -20,13 +20,16 @@ use tide_openssl::TlsListener; use kanidm::tracing_tree::TreeMiddleware; use tracing::{error, info}; +use compact_jwt::{Jws, JwsSigner, JwsUnverified, JwsValidator}; + #[derive(Clone)] pub struct AppState { pub status_ref: &'static StatusActor, pub qe_w_ref: &'static QueryServerWriteV1, pub qe_r_ref: &'static QueryServerReadV1, // Store the token management parts. - pub bundy_handle: std::sync::Arc, + pub jws_signer: std::sync::Arc, + pub jws_validator: std::sync::Arc, } pub trait RequestExtensions { @@ -69,7 +72,7 @@ impl RequestExtensions for tide::Request { fn get_current_auth_session_id(&self) -> Option { // We see if there is a signed header copy first. - let kref = &self.state().bundy_handle; + let kref = &self.state().jws_validator; self.header("X-KANIDM-AUTH-SESSION-ID") .and_then(|hv| { // Get the first header value. @@ -78,8 +81,12 @@ impl RequestExtensions for tide::Request { .and_then(|h| { // Take the token str and attempt to decrypt // Attempt to re-inflate a uuid from bytes. - let uat: Option = kref.verify(h.as_str()).ok(); - uat + JwsUnverified::from_str(h.as_str()).ok() + }) + .and_then(|jwsu| { + jwsu.validate(kref) + .map(|jws: Jws| jws.inner.sessionid) + .ok() }) // If not there, get from the cookie instead. .or_else(|| self.session().get::("auth-session-id")) @@ -289,24 +296,26 @@ pub fn create_https_server( opt_tls_params: Option<&TlsConfiguration>, role: ServerRole, cookie_key: &[u8; 32], - bundy_key: &str, + jws_signer: JwsSigner, status_ref: &'static StatusActor, qe_w_ref: &'static QueryServerWriteV1, qe_r_ref: &'static QueryServerReadV1, ) -> Result<(), ()> { info!("WEB_UI_PKG_PATH -> {}", env!("KANIDM_WEB_UI_PKG_PATH")); - let bundy_handle = bundy::hs512::HS512::from_str(bundy_key).map_err(|e| { - error!(?e, "Failed to generate bundy handle"); + let jws_validator = jws_signer.get_validator().map_err(|e| { + error!(?e, "Failed to get jws validator"); })?; - let bundy_handle = std::sync::Arc::new(bundy_handle); + let jws_validator = std::sync::Arc::new(jws_validator); + let jws_signer = std::sync::Arc::new(jws_signer); let mut tserver = tide::Server::with_state(AppState { status_ref, qe_w_ref, qe_r_ref, - bundy_handle, + jws_signer, + jws_validator, }); // tide::log::with_level(tide::log::LevelFilter::Debug); diff --git a/kanidmd/score/src/https/v1.rs b/kanidmd/score/src/https/v1.rs index a70c6d636..e5eab99b7 100644 --- a/kanidmd/score/src/https/v1.rs +++ b/kanidmd/score/src/https/v1.rs @@ -13,6 +13,14 @@ use kanidm_proto::v1::{ use super::{to_tide_response, AppState, RequestExtensions}; use async_std::task; +use compact_jwt::Jws; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) struct SessionId { + pub sessionid: Uuid, +} pub async fn create(mut req: tide::Request) -> tide::Result { let uat = req.get_current_uat(); @@ -817,15 +825,25 @@ pub async fn auth(mut req: tide::Request) -> tide::Result { msession.remove("auth-session-id"); msession .insert("auth-session-id", sessionid) - .map_err(|_| OperationError::InvalidSessionState) + .map_err(|e| { + error!(?e); + OperationError::InvalidSessionState + }) .and_then(|_| { - let kref = &req.state().bundy_handle; + let kref = &req.state().jws_signer; + + let jws = Jws { + inner: SessionId { sessionid }, + }; // Get the header token ready. - kref.sign(&sessionid) - .map(|t| { - auth_session_id_tok = Some(t); + jws.sign(&kref) + .map(|jwss| { + auth_session_id_tok = Some(jwss.to_string()); + }) + .map_err(|e| { + error!(?e); + OperationError::InvalidSessionState }) - .map_err(|_| OperationError::InvalidSessionState) }) .map(|_| ProtoAuthState::Choose(allowed)) } @@ -836,15 +854,24 @@ pub async fn auth(mut req: tide::Request) -> tide::Result { msession.remove("auth-session-id"); msession .insert("auth-session-id", sessionid) - .map_err(|_| OperationError::InvalidSessionState) + .map_err(|e| { + error!(?e); + OperationError::InvalidSessionState + }) .and_then(|_| { - let kref = &req.state().bundy_handle; + let kref = &req.state().jws_signer; // Get the header token ready. - kref.sign(&sessionid) - .map(|t| { - auth_session_id_tok = Some(t); + let jws = Jws { + inner: SessionId { sessionid }, + }; + jws.sign(&kref) + .map(|jwss| { + auth_session_id_tok = Some(jwss.to_string()); + }) + .map_err(|e| { + error!(?e); + OperationError::InvalidSessionState }) - .map_err(|_| OperationError::InvalidSessionState) }) .map(|_| ProtoAuthState::Continue(allowed)) } diff --git a/kanidmd/score/src/lib.rs b/kanidmd/score/src/lib.rs index 3a34b83ad..d970f6f29 100644 --- a/kanidmd/score/src/lib.rs +++ b/kanidmd/score/src/lib.rs @@ -51,6 +51,7 @@ use kanidm::utils::{duration_from_epoch_now, touch_file_or_quit}; use kanidm_proto::v1::OperationError; use async_std::task; +use compact_jwt::JwsSigner; // === internal setup helpers @@ -585,10 +586,10 @@ pub async fn create_server_core(config: Configuration, config_test: bool) -> Res // Extract any configuration from the IDMS that we may need. // For now we just do this per run, but we need to extract this from the db later. - let bundy_key = match bundy::hs512::HS512::generate_key() { + let jws_signer = match JwsSigner::generate_hs256() { Ok(k) => k, Err(e) => { - error!("Unable to setup bundy -> {:?}", e); + error!("Unable to setup jws signer -> {:?}", e); return Err(()); } }; @@ -692,7 +693,7 @@ pub async fn create_server_core(config: Configuration, config_test: bool) -> Res config.tls_config.as_ref(), config.role, &cookie_key, - &bundy_key, + jws_signer, status_ref, server_write_ref, server_read_ref, diff --git a/kanidmd/src/lib/constants/acp.rs b/kanidmd/src/lib/constants/acp.rs index 9514f6d53..fd8fca611 100644 --- a/kanidmd/src/lib/constants/acp.rs +++ b/kanidmd/src/lib/constants/acp.rs @@ -954,11 +954,13 @@ pub const JSON_IDM_ACP_DOMAIN_ADMIN_PRIV_V1: &str = r#"{ "domain_name", "domain_ssid", "domain_uuid", - "domain_token_key" + "fernet_private_key_str", + "es256_private_key_der" ], "acp_modify_removedattr": [ "domain_ssid", - "domain_token_key" + "fernet_private_key_str", + "es256_private_key_der" ], "acp_modify_presentattr": [ "domain_ssid" diff --git a/kanidmd/src/lib/constants/entries.rs b/kanidmd/src/lib/constants/entries.rs index c27117081..964bf6a24 100644 --- a/kanidmd/src/lib/constants/entries.rs +++ b/kanidmd/src/lib/constants/entries.rs @@ -402,7 +402,7 @@ pub const JSON_SYSTEM_INFO_V1: &str = r#"{ "class": ["object", "system_info", "system"], "uuid": ["00000000-0000-0000-0000-ffffff000001"], "description": ["System (local) info and metadata object."], - "version": ["5"] + "version": ["6"] } }"#; diff --git a/kanidmd/src/lib/constants/schema.rs b/kanidmd/src/lib/constants/schema.rs index 685472360..b78108903 100644 --- a/kanidmd/src/lib/constants/schema.rs +++ b/kanidmd/src/lib/constants/schema.rs @@ -274,6 +274,7 @@ pub const JSON_SCHEMA_ATTR_DOMAIN_SSID: &str = r#"{ ] } }"#; + pub const JSON_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = r#"{ "attrs": { "class": [ @@ -282,7 +283,7 @@ pub const JSON_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = r#"{ "attributetype" ], "description": [ - "The domains token signing key, which is shared between IDM servers." + "The domain token encryption private key (NOT USED)." ], "index": [], "unique": [ @@ -303,6 +304,35 @@ pub const JSON_SCHEMA_ATTR_DOMAIN_TOKEN_KEY: &str = r#"{ } }"#; +pub const JSON_SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR: &str = r#"{ + "attrs": { + "class": [ + "object", + "system", + "attributetype" + ], + "description": [ + "The token encryption private key." + ], + "index": [], + "unique": [ + "false" + ], + "multivalue": [ + "false" + ], + "attributename": [ + "fernet_private_key_str" + ], + "syntax": [ + "SECRET_UTF8STRING" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000095" + ] + } +}"#; + pub const JSON_SCHEMA_ATTR_GIDNUMBER: &str = r#"{ "attrs": { "class": [ @@ -953,7 +983,8 @@ pub const JSON_SCHEMA_CLASS_DOMAIN_INFO: &str = r#" "name", "domain_uuid", "domain_name", - "domain_token_key" + "fernet_private_key_str", + "es256_private_key_der" ], "uuid": [ "00000000-0000-0000-0000-ffff00000052" diff --git a/kanidmd/src/lib/constants/uuids.rs b/kanidmd/src/lib/constants/uuids.rs index 375512ee7..7b54f6067 100644 --- a/kanidmd/src/lib/constants/uuids.rs +++ b/kanidmd/src/lib/constants/uuids.rs @@ -160,6 +160,8 @@ pub const _UUID_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE: Uuid = pub const _UUID_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER: Uuid = uuid!("00000000-0000-0000-0000-ffff00000093"); pub const _UUID_SCHEMA_CLASS_ORGPERSON: Uuid = uuid!("00000000-0000-0000-0000-ffff00000094"); +pub const UUID_SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000095"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. diff --git a/kanidmd/src/lib/credential/mod.rs b/kanidmd/src/lib/credential/mod.rs index 225057984..bdf598f38 100644 --- a/kanidmd/src/lib/credential/mod.rs +++ b/kanidmd/src/lib/credential/mod.rs @@ -269,7 +269,7 @@ pub struct Credential { } #[derive(Clone, Debug)] -/// The typo of credential that is stored. Each of these represents a full set of 'what is required' +/// The type of credential that is stored. Each of these represents a full set of 'what is required' /// to complete an authentication session. The reason to have these typed like this is so we can /// apply policy later to what classes or levels of credentials can be used. We use these types /// to also know what type of auth session handler to initiate. diff --git a/kanidmd/src/lib/entry.rs b/kanidmd/src/lib/entry.rs index 048996722..e1a56c956 100644 --- a/kanidmd/src/lib/entry.rs +++ b/kanidmd/src/lib/entry.rs @@ -427,10 +427,14 @@ impl Entry { }).collect(); vs.unwrap() } - "domain_token_key" => { + "domain_token_key" | "fernet_private_key_str" => { let vs: Option = vs.into_iter().map(|v| Value::new_secret_str(&v)).collect(); vs.unwrap() } + "es256_private_key_der" => { + let vs: Option = vs.into_iter().map(|v| Value::new_privatebinary_base64(&v)).collect(); + vs.unwrap() + } ia => { warn!("WARNING: Allowing invalid attribute {} to be interpretted as UTF8 string. YOU MAY ENCOUNTER ODD BEHAVIOUR!!!", ia); let vs: Option = vs.into_iter().map(|v| Value::new_utf8(v)).collect(); diff --git a/kanidmd/src/lib/idm/authsession.rs b/kanidmd/src/lib/idm/authsession.rs index 4c93ba8d4..4eb372a14 100644 --- a/kanidmd/src/lib/idm/authsession.rs +++ b/kanidmd/src/lib/idm/authsession.rs @@ -22,7 +22,7 @@ use crate::credential::webauthn::WebauthnDomainConfig; use std::time::Duration; use uuid::Uuid; // use webauthn_rs::proto::Credential as WebauthnCredential; -use bundy::hs512::HS512; +use compact_jwt::{Jws, JwsSigner}; pub use std::collections::BTreeSet as Set; use webauthn_rs::proto::RequestChallengeResponse; use webauthn_rs::{AuthenticationState, Webauthn}; @@ -667,7 +667,7 @@ impl AuthSession { async_tx: &Sender, webauthn: &Webauthn, pw_badlist_set: Option<&HashSet>, - uat_bundy_hmac: &HS512, + uat_jwt_signer: &JwsSigner, ) -> Result { let (next_state, response) = match &mut self.state { AuthSessionState::Init(_) | AuthSessionState::Success | AuthSessionState::Denied(_) => { @@ -699,11 +699,19 @@ impl AuthSession { .to_userauthtoken(tracing_id, *time, auth_type) .ok_or(OperationError::InvalidState)?; + let jwt = Jws { inner: uat }; + // Now encrypt and prepare the token for return to the client. - let token = uat_bundy_hmac.sign(&uat).map_err(|e| { - admin_error!(?e, "Failed to sign UserAuthToken"); - OperationError::InvalidState - })?; + let token = jwt + // Do we want to embed this? Or just give the URL? I think we embed + // as we only need the client to be able to check it's not tampered, but + // this isn't a root of trust. + .sign_embed_public_jwk(&uat_jwt_signer) + .map(|jwts| jwts.to_string()) + .map_err(|e| { + admin_error!(?e, "Failed to sign UserAuthToken to Jwt"); + OperationError::InvalidState + })?; ( Some(AuthSessionState::Success), @@ -790,8 +798,7 @@ mod tests { use tokio::sync::mpsc::unbounded_channel as unbounded; use webauthn_authenticator_rs::{softtok::U2FSoft, WebauthnAuthenticator}; - use bundy::hs512::HS512; - use std::str::FromStr; + use compact_jwt::JwsSigner; fn create_pw_badlist_cache() -> HashSet { let mut s = HashSet::new(); @@ -807,9 +814,8 @@ mod tests { }) } - fn create_hs512() -> HS512 { - let pkey_str = HS512::generate_key().unwrap(); - HS512::from_str(&pkey_str).unwrap() + fn create_jwt_signer() -> JwsSigner { + JwsSigner::generate_es256().expect("failed to construct signer.") } #[test] @@ -891,14 +897,14 @@ mod tests { start_password_session!(&mut audit, account, &webauthn); let attempt = AuthCredential::Password("bad_password".to_string()); - let hs512 = create_hs512(); + let jws_signer = create_jwt_signer(); match session.validate_creds( &attempt, &Duration::from_secs(0), &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(_)) => {} _ => panic!(), @@ -916,7 +922,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Success(_)) => {} _ => panic!(), @@ -929,7 +935,7 @@ mod tests { #[test] fn test_idm_authsession_simple_password_badlist() { let _ = tracing_tree::test_init(); - let hs512 = create_hs512(); + let jws_signer = create_jwt_signer(); let webauthn = create_webauthn(); // create the ent let mut account = entry_str_to_account!(JSON_ADMIN_V1); @@ -951,7 +957,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG), _ => panic!(), @@ -1017,8 +1023,8 @@ mod tests { #[test] fn test_idm_authsession_totp_password_mech() { let _ = tracing_tree::test_init(); - let hs512 = create_hs512(); let webauthn = create_webauthn(); + let jws_signer = create_jwt_signer(); // create the ent let mut account = entry_str_to_account!(JSON_ADMIN_V1); @@ -1061,7 +1067,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1081,7 +1087,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1098,7 +1104,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), _ => panic!(), @@ -1117,7 +1123,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1128,7 +1134,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -1147,7 +1153,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1158,7 +1164,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Success(_)) => {} _ => panic!(), @@ -1173,7 +1179,7 @@ mod tests { fn test_idm_authsession_password_mfa_badlist() { let _ = tracing_tree::test_init(); let webauthn = create_webauthn(); - let hs512 = create_hs512(); + let jws_signer = create_jwt_signer(); // create the ent let mut account = entry_str_to_account!(JSON_ADMIN_V1); @@ -1214,7 +1220,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1225,7 +1231,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG), _ => panic!(), @@ -1308,7 +1314,7 @@ mod tests { let mut account = entry_str_to_account!(JSON_ADMIN_V1); let (webauthn, mut wa, wan_cred) = setup_webauthn(account.name.as_str()); - let hs512 = create_hs512(); + let jws_signer = create_jwt_signer(); // Now create the credential for the account. let cred = Credential::new_webauthn_only("soft".to_string(), wan_cred); @@ -1327,7 +1333,7 @@ mod tests { &async_tx, &webauthn, None, - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1348,7 +1354,7 @@ mod tests { &async_tx, &webauthn, None, - &hs512, + &jws_signer, ) { Ok(AuthState::Success(_)) => {} _ => panic!(), @@ -1377,7 +1383,7 @@ mod tests { &async_tx, &webauthn, None, - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), @@ -1419,7 +1425,7 @@ mod tests { &async_tx, &webauthn, None, - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), @@ -1439,7 +1445,7 @@ mod tests { let mut account = entry_str_to_account!(JSON_ADMIN_V1); let (webauthn, mut wa, wan_cred) = setup_webauthn(account.name.as_str()); - let hs512 = create_hs512(); + let jws_signer = create_jwt_signer(); let pw_good = "test_password"; let pw_bad = "bad_password"; @@ -1463,7 +1469,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1481,7 +1487,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1509,7 +1515,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), @@ -1532,7 +1538,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1543,7 +1549,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -1572,7 +1578,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1583,7 +1589,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Success(_)) => {} _ => panic!(), @@ -1609,7 +1615,7 @@ mod tests { let mut account = entry_str_to_account!(JSON_ADMIN_V1); let (webauthn, mut wa, wan_cred) = setup_webauthn(account.name.as_str()); - let hs512 = create_hs512(); + let jws_signer = create_jwt_signer(); let totp = Totp::generate_secure(TOTP_DEFAULT_STEP); let totp_good = totp @@ -1644,7 +1650,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1662,7 +1668,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG), _ => panic!(), @@ -1688,7 +1694,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG), _ => panic!(), @@ -1711,7 +1717,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1722,7 +1728,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -1746,7 +1752,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1757,7 +1763,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -1775,7 +1781,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1786,7 +1792,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Success(_)) => {} _ => panic!(), @@ -1809,7 +1815,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1820,7 +1826,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Success(_)) => {} _ => panic!(), @@ -1840,7 +1846,7 @@ mod tests { #[test] fn test_idm_authsession_backup_code_mech() { let _ = tracing_tree::test_init(); - let hs512 = create_hs512(); + let jws_signer = create_jwt_signer(); let webauthn = create_webauthn(); // create the ent let mut account = entry_str_to_account!(JSON_ADMIN_V1); @@ -1892,7 +1898,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG), _ => panic!(), @@ -1909,7 +1915,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_BACKUPCODE_MSG), _ => panic!(), @@ -1927,7 +1933,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1938,7 +1944,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG), _ => panic!(), @@ -1962,7 +1968,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -1973,7 +1979,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Success(_)) => {} _ => panic!(), @@ -1998,7 +2004,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]), _ => panic!(), @@ -2009,7 +2015,7 @@ mod tests { &async_tx, &webauthn, Some(&pw_badlist_cache), - &hs512, + &jws_signer, ) { Ok(AuthState::Success(_)) => {} _ => panic!(), diff --git a/kanidmd/src/lib/idm/oauth2.rs b/kanidmd/src/lib/idm/oauth2.rs index ef364d11a..c74c508b5 100644 --- a/kanidmd/src/lib/idm/oauth2.rs +++ b/kanidmd/src/lib/idm/oauth2.rs @@ -1129,6 +1129,10 @@ impl Oauth2ResourceServersReadTransaction { let id_token_signing_alg_values_supported = match &o2rs.jws_signer { JwsSigner::ES256 { .. } => vec![IdTokenSignAlg::ES256], JwsSigner::RS256 { .. } => vec![IdTokenSignAlg::RS256], + JwsSigner::HS256 { .. } => { + admin_warn!("Invalid oauth2 configuration - HS256 is not supported!"); + vec![] + } }; let userinfo_signing_alg_values_supported = None; diff --git a/kanidmd/src/lib/idm/server.rs b/kanidmd/src/lib/idm/server.rs index cfb5a205b..cfe9f768c 100644 --- a/kanidmd/src/lib/idm/server.rs +++ b/kanidmd/src/lib/idm/server.rs @@ -41,9 +41,9 @@ use kanidm_proto::v1::{ AuthType, BackupCodesView, CredentialStatus, RadiusAuthToken, SetCredentialResponse, UnixGroupToken, UnixUserToken, UserAuthToken, }; -use std::str::FromStr; -use bundy::hs512::HS512; +use compact_jwt::{Jws, JwsSigner, JwsUnverified, JwsValidator}; +use fernet::Fernet; use tokio::sync::mpsc::{ unbounded_channel as unbounded, UnboundedReceiver as Receiver, UnboundedSender as Sender, @@ -57,14 +57,14 @@ use futures::task as futures_task; use concread::{ bptree::{BptreeMap, BptreeMapWriteTxn}, - CowCell, -}; -use concread::{ cowcell::{CowCellReadTxn, CowCellWriteTxn}, hashmap::HashMap, + CowCell, }; + use rand::prelude::*; use std::convert::TryFrom; +use std::str::FromStr; use std::{sync::Arc, time::Duration}; use tokio::sync::Mutex; use url::Url; @@ -79,6 +79,8 @@ use tracing::trace; type AuthSessionMutex = Arc>; type CredSoftLockMutex = Arc>; +// type CredUpdateSessionMutex = Arc>; + pub struct IdmServer { // There is a good reason to keep this single thread - it // means that limits to sessions can be easily applied and checked to @@ -89,6 +91,7 @@ pub struct IdmServer { softlocks: HashMap, /// A set of in progress MFA registrations mfareg_sessions: BptreeMap, + // cred_update_sessions: BptreeMap, /// Reference to the query server. qs: QueryServer, /// The configured crypto policy for the IDM server. Later this could be transactional and loaded from the db similar to access. But today it's just to allow dynamic pbkdf2rounds @@ -98,7 +101,10 @@ pub struct IdmServer { webauthn: Webauthn, pw_badlist_cache: Arc>>, oauth2rs: Arc, - uat_bundy_hmac: Arc>, + + uat_jwt_signer: Arc>, + uat_jwt_validator: Arc>, + token_enc_key: Arc>, } /// Contains methods that require writes, but in the context of writing to the idm in memory structures (maybe the query server too). This is things like authentication. @@ -114,13 +120,14 @@ pub struct IdmServerAuthTransaction<'a> { async_tx: Sender, webauthn: &'a Webauthn, pw_badlist_cache: CowCellReadTxn>, - uat_bundy_hmac: CowCellReadTxn, + uat_jwt_signer: CowCellReadTxn, + uat_jwt_validator: CowCellReadTxn, } /// This contains read-only methods, like getting users, groups and other structured content. pub struct IdmServerProxyReadTransaction<'a> { pub qs_read: QueryServerReadTransaction<'a>, - uat_bundy_hmac: CowCellReadTxn, + uat_jwt_validator: CowCellReadTxn, oauth2rs: Oauth2ResourceServersReadTransaction, } @@ -134,7 +141,9 @@ pub struct IdmServerProxyWriteTransaction<'a> { crypto_policy: &'a CryptoPolicy, webauthn: &'a Webauthn, pw_badlist_cache: CowCellWriteTxn<'a, HashSet>, - uat_bundy_hmac: CowCellWriteTxn<'a, HS512>, + uat_jwt_signer: CowCellWriteTxn<'a, JwsSigner>, + uat_jwt_validator: CowCellWriteTxn<'a, JwsValidator>, + token_enc_key: CowCellWriteTxn<'a, Fernet>, oauth2rs: Oauth2ResourceServersWriteTransaction<'a>, } @@ -160,11 +169,12 @@ impl IdmServer { let (async_tx, async_rx) = unbounded(); // Get the domain name, as the relying party id. - let (rp_id, token_key, pw_badlist_set, oauth2rs_set) = { + let (rp_id, fernet_private_key, es256_private_key, pw_badlist_set, oauth2rs_set) = { let qs_read = task::block_on(qs.read_async()); ( qs_read.get_domain_name().to_string(), - qs_read.get_domain_token_key()?, + qs_read.get_domain_fernet_private_key()?, + qs_read.get_domain_es256_private_key()?, qs_read.get_password_badlist()?, // Add a read/reload of all oauth2 configurations. qs_read.get_oauth2rs_set()?, @@ -205,11 +215,24 @@ impl IdmServer { }); // Setup our auth token signing key. - let bundy_handle = HS512::from_str(&token_key).map_err(|e| { - admin_error!("Failed to generate uat_bundy_hmac - {:?}", e); - OperationError::InvalidState + let fernet_key = Fernet::new(&fernet_private_key).ok_or_else(|| { + admin_error!("Unable to load Fernet encryption key"); + OperationError::CryptographyError })?; - let uat_bundy_hmac = Arc::new(CowCell::new(bundy_handle)); + let token_enc_key = Arc::new(CowCell::new(fernet_key)); + + let jwt_signer = JwsSigner::from_es256_der(&es256_private_key).map_err(|e| { + admin_error!(err = ?e, "Unable to load ES256 JwsSigner from DER"); + OperationError::CryptographyError + })?; + + let jwt_validator = jwt_signer.get_validator().map_err(|e| { + admin_error!(err = ?e, "Unable to load ES256 JwsValidator from JwsSigner"); + OperationError::CryptographyError + })?; + + let uat_jwt_signer = Arc::new(CowCell::new(jwt_signer)); + let uat_jwt_validator = Arc::new(CowCell::new(jwt_validator)); let oauth2rs = Oauth2ResourceServers::try_from((oauth2rs_set, origin_url)).map_err(|e| { @@ -228,7 +251,9 @@ impl IdmServer { async_tx, webauthn, pw_badlist_cache: Arc::new(CowCell::new(pw_badlist_set)), - uat_bundy_hmac, + uat_jwt_signer, + uat_jwt_validator, + token_enc_key, oauth2rs: Arc::new(oauth2rs), }, IdmServerDelayed { async_rx }, @@ -256,7 +281,8 @@ impl IdmServer { async_tx: self.async_tx.clone(), webauthn: &self.webauthn, pw_badlist_cache: self.pw_badlist_cache.read(), - uat_bundy_hmac: self.uat_bundy_hmac.read(), + uat_jwt_signer: self.uat_jwt_signer.read(), + uat_jwt_validator: self.uat_jwt_validator.read(), } } @@ -268,7 +294,7 @@ impl IdmServer { pub async fn proxy_read_async(&self) -> IdmServerProxyReadTransaction<'_> { IdmServerProxyReadTransaction { qs_read: self.qs.read_async().await, - uat_bundy_hmac: self.uat_bundy_hmac.read(), + uat_jwt_validator: self.uat_jwt_validator.read(), oauth2rs: self.oauth2rs.read(), } } @@ -291,7 +317,9 @@ impl IdmServer { crypto_policy: &self.crypto_policy, webauthn: &self.webauthn, pw_badlist_cache: self.pw_badlist_cache.write(), - uat_bundy_hmac: self.uat_bundy_hmac.write(), + uat_jwt_signer: self.uat_jwt_signer.write(), + uat_jwt_validator: self.uat_jwt_validator.write(), + token_enc_key: self.token_enc_key.write(), oauth2rs: self.oauth2rs.write(), } } @@ -347,7 +375,7 @@ pub(crate) trait IdmServerTransaction<'a> { fn get_qs_txn(&self) -> &Self::QsTransactionType; - fn get_uat_bundy_txn(&self) -> &HS512; + fn get_uat_validator_txn(&self) -> &JwsValidator; fn validate_and_parse_uat( &self, @@ -355,17 +383,24 @@ pub(crate) trait IdmServerTransaction<'a> { ct: Duration, ) -> Result { // Given the token string, validate and recreate the UAT - let bref = self.get_uat_bundy_txn(); + let jws_validator = self.get_uat_validator_txn(); - let uat: UserAuthToken = - token - .ok_or(OperationError::NotAuthenticated) - .and_then(|token| { - bref.verify(token).map_err(|e| { + let uat: UserAuthToken = token + .ok_or(OperationError::NotAuthenticated) + .and_then(|s| { + JwsUnverified::from_str(s).map_err(|e| { + security_info!(?e, "Unable to decode token"); + OperationError::NotAuthenticated + }) + }) + .and_then(|jwtu| { + jwtu.validate(jws_validator) + .map_err(|e| { security_info!(?e, "Unable to verify token"); OperationError::NotAuthenticated }) - })?; + .map(|t: Jws| t.inner) + })?; if time::OffsetDateTime::unix_epoch() + ct >= uat.expiry { security_info!("Session expired"); @@ -469,8 +504,8 @@ impl<'a> IdmServerTransaction<'a> for IdmServerAuthTransaction<'a> { &self.qs_read } - fn get_uat_bundy_txn(&self) -> &HS512 { - &*self.uat_bundy_hmac + fn get_uat_validator_txn(&self) -> &JwsValidator { + &*self.uat_jwt_validator } } @@ -742,7 +777,7 @@ impl<'a> IdmServerAuthTransaction<'a> { &self.async_tx, self.webauthn, pw_badlist_cache, - &*self.uat_bundy_hmac, + &*self.uat_jwt_signer, ) .map(|aus| { // Inspect the result: @@ -1009,8 +1044,8 @@ impl<'a> IdmServerTransaction<'a> for IdmServerProxyReadTransaction<'a> { &self.qs_read } - fn get_uat_bundy_txn(&self) -> &HS512 { - &*self.uat_bundy_hmac + fn get_uat_validator_txn(&self) -> &JwsValidator { + &*self.uat_jwt_validator } } @@ -1186,8 +1221,8 @@ impl<'a> IdmServerTransaction<'a> for IdmServerProxyWriteTransaction<'a> { &self.qs_write } - fn get_uat_bundy_txn(&self) -> &HS512 { - &*self.uat_bundy_hmac + fn get_uat_validator_txn(&self) -> &JwsValidator { + &*self.uat_jwt_validator } } @@ -1999,20 +2034,43 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { if self.qs_write.get_changed_domain() { // reload token_key? self.qs_write - .get_domain_token_key() + .get_domain_fernet_private_key() .and_then(|token_key| { - HS512::from_str(&token_key).map_err(|e| { - admin_error!("Failed to generate uat_bundy_hmac - {:?}", e); + Fernet::new(&token_key).ok_or_else(|| { + admin_error!("Failed to generate token_enc_key"); OperationError::InvalidState }) }) .map(|new_handle| { - *self.uat_bundy_hmac = new_handle; + *self.token_enc_key = new_handle; + })?; + self.qs_write + .get_domain_es256_private_key() + .and_then(|key_der| { + JwsSigner::from_es256_der(&key_der).map_err(|e| { + admin_error!("Failed to generate uat_jwt_signer - {:?}", e); + OperationError::InvalidState + }) + }) + .and_then(|signer| { + signer + .get_validator() + .map_err(|e| { + admin_error!("Failed to generate uat_jwt_validator - {:?}", e); + OperationError::InvalidState + }) + .map(|validator| (signer, validator)) + }) + .map(|(new_signer, new_validator)| { + *self.uat_jwt_signer = new_signer; + *self.uat_jwt_validator = new_validator; })?; } // Commit everything. self.oauth2rs.commit(); - self.uat_bundy_hmac.commit(); + self.uat_jwt_signer.commit(); + self.uat_jwt_validator.commit(); + self.token_enc_key.commit(); self.pw_badlist_cache.commit(); self.mfareg_sessions.commit(); self.qs_write.commit() @@ -3891,11 +3949,21 @@ mod tests { // Now reset the token_key - we can cheat and push this // through the migrate 3 to 4 code. + // + // fernet_private_key_str + // es256_private_key_der let idms_prox_write = idms.proxy_write(ct.clone()); - idms_prox_write - .qs_write - .migrate_3_to_4() - .expect("Failed to reset domain token key"); + let me_reset_tokens = unsafe { + ModifyEvent::new_internal_invalid( + filter!(f_eq("uuid", PartialValue::new_uuidr(&UUID_DOMAIN_INFO))), + ModifyList::new_list(vec![ + Modify::Purged(AttrString::from("fernet_private_key_str")), + Modify::Purged(AttrString::from("es256_private_key_der")), + Modify::Purged(AttrString::from("domain_token_key")), + ]), + ) + }; + assert!(idms_prox_write.qs_write.modify(&me_reset_tokens).is_ok()); assert!(idms_prox_write.commit().is_ok()); // Check the old token is invalid, due to reload. let new_token = check_admin_password(idms, TEST_PASSWORD); diff --git a/kanidmd/src/lib/plugins/domain.rs b/kanidmd/src/lib/plugins/domain.rs index 63baa92f0..1b91aa01e 100644 --- a/kanidmd/src/lib/plugins/domain.rs +++ b/kanidmd/src/lib/plugins/domain.rs @@ -8,7 +8,7 @@ use crate::plugins::Plugin; use crate::event::{CreateEvent, ModifyEvent}; use crate::prelude::*; -use bundy::hs512::HS512; +use compact_jwt::JwsSigner; use kanidm_proto::v1::OperationError; use std::iter::once; use tracing::trace; @@ -44,15 +44,22 @@ impl Plugin for Domain { e.set_ava("domain_name", once(n)); trace!("plugin_domain: Applying domain_name transform"); } - if !e.attribute_pres("domain_token_key") { - let k = HS512::generate_key() - .map(|k| Value::new_secret_str(&k)) + if !e.attribute_pres("fernet_private_key_str") { + security_info!("regenerating domain token encryption key"); + let k = fernet::Fernet::generate_key(); + let v = Value::new_secret_str(&k); + e.add_ava("fernet_private_key_str", v); + } + if !e.attribute_pres("es256_private_key_der") { + security_info!("regenerating domain es256 private key"); + let der = JwsSigner::generate_es256() + .and_then(|jws| jws.private_key_to_der()) .map_err(|e| { - admin_error!(err = ?e, "Failed to generate domain_token_key"); - OperationError::InvalidState + admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key"); + OperationError::CryptographyError })?; - e.set_ava("domain_token_key", once(k)); - trace!("plugin_domain: Applying domain_token_key transform"); + let v = Value::new_privatebinary(&der); + e.add_ava("es256_private_key_der", v); } trace!(?e); Ok(()) @@ -71,15 +78,22 @@ impl Plugin for Domain { if e.attribute_equality("class", &PVCLASS_DOMAIN_INFO) && e.attribute_equality("uuid", &PVUUID_DOMAIN_INFO) { - if !e.attribute_pres("domain_token_key") { - let k = HS512::generate_key() - .map(|k| Value::new_secret_str(&k)) + if !e.attribute_pres("fernet_private_key_str") { + security_info!("regenerating domain token encryption key"); + let k = fernet::Fernet::generate_key(); + let v = Value::new_secret_str(&k); + e.add_ava("fernet_private_key_str", v); + } + if !e.attribute_pres("es256_private_key_der") { + security_info!("regenerating domain es256 private key"); + let der = JwsSigner::generate_es256() + .and_then(|jws| jws.private_key_to_der()) .map_err(|e| { - admin_error!(err = ?e, "Failed to generate domain_token_key"); - OperationError::InvalidState + admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key"); + OperationError::CryptographyError })?; - e.set_ava("domain_token_key", once(k)); - trace!("plugin_domain: Applying domain_token_key transform"); + let v = Value::new_privatebinary(&der); + e.add_ava("es256_private_key_der", v); } trace!(?e); Ok(()) diff --git a/kanidmd/src/lib/plugins/protected.rs b/kanidmd/src/lib/plugins/protected.rs index 22daa2597..820841918 100644 --- a/kanidmd/src/lib/plugins/protected.rs +++ b/kanidmd/src/lib/plugins/protected.rs @@ -23,7 +23,8 @@ lazy_static! { m.insert("may"); // Allow modification of some domain info types for local configuration. m.insert("domain_ssid"); - m.insert("domain_token_key"); + m.insert("fernet_private_key_str"); + m.insert("es256_private_key_der"); m.insert("badlist_password"); m }; @@ -193,10 +194,10 @@ mod tests { ], "acp_search_attr": ["name", "class", "uuid", "classname", "attributename"], "acp_modify_class": ["system", "domain_info"], - "acp_modify_removedattr": ["class", "displayname", "may", "must", "domain_name", "domain_uuid", "domain_ssid", "domain_token_key"], - "acp_modify_presentattr": ["class", "displayname", "may", "must", "domain_name", "domain_uuid", "domain_ssid", "domain_token_key"], + "acp_modify_removedattr": ["class", "displayname", "may", "must", "domain_name", "domain_uuid", "domain_ssid", "fernet_private_key_str", "es256_private_key_der"], + "acp_modify_presentattr": ["class", "displayname", "may", "must", "domain_name", "domain_uuid", "domain_ssid", "fernet_private_key_str", "es256_private_key_der"], "acp_create_class": ["object", "person", "system", "domain_info"], - "acp_create_attr": ["name", "class", "description", "displayname", "domain_name", "domain_uuid", "domain_ssid", "uuid", "domain_token_key"] + "acp_create_attr": ["name", "class", "description", "displayname", "domain_name", "domain_uuid", "domain_ssid", "uuid", "fernet_private_key_str", "es256_private_key_der"] } }"#; @@ -330,7 +331,8 @@ mod tests { "description": ["Demonstration of a remote domain's info being created for uuid generation"], "domain_name": ["example.net.au"], "domain_ssid": ["Example_Wifi"], - "domain_token_key": ["ABCD"] + "fernet_private_key_str": ["ABCD"], + "es256_private_key_der" : ["MTIz"] } }"#, ); @@ -368,7 +370,8 @@ mod tests { "description": ["Demonstration of a remote domain's info being created for uuid generation"], "domain_name": ["example.net.au"], "domain_ssid": ["Example_Wifi"], - "domain_token_key": ["ABCD"] + "fernet_private_key_str": ["ABCD"], + "es256_private_key_der" : ["MTIz"] } }"#, ); @@ -397,7 +400,8 @@ mod tests { "description": ["Demonstration of a remote domain's info being created for uuid generation"], "domain_name": ["example.net.au"], "domain_ssid": ["Example_Wifi"], - "domain_token_key": ["ABCD"] + "fernet_private_key_str": ["ABCD"], + "es256_private_key_der" : ["MTIz"] } }"#, ); diff --git a/kanidmd/src/lib/server.rs b/kanidmd/src/lib/server.rs index da226f26b..9d6f57bd9 100644 --- a/kanidmd/src/lib/server.rs +++ b/kanidmd/src/lib/server.rs @@ -713,15 +713,28 @@ pub trait QueryServerTransaction<'a> { }) } - fn get_domain_token_key(&self) -> Result { + fn get_domain_fernet_private_key(&self) -> Result { self.internal_search_uuid(&UUID_DOMAIN_INFO) .and_then(|e| { - e.get_ava_single_secret("domain_token_key") + e.get_ava_single_secret("fernet_private_key_str") .map(str::to_string) .ok_or(OperationError::InvalidEntryState) }) .map_err(|e| { - admin_error!(?e, "Error getting domain token key"); + admin_error!(?e, "Error getting domain fernet key"); + e + }) + } + + fn get_domain_es256_private_key(&self) -> Result, OperationError> { + self.internal_search_uuid(&UUID_DOMAIN_INFO) + .and_then(|e| { + e.get_ava_single_private_binary("es256_private_key_der") + .map(|s| s.to_vec()) + .ok_or(OperationError::InvalidEntryState) + }) + .map_err(|e| { + admin_error!(?e, "Error getting domain es256 key"); e }) } @@ -1080,6 +1093,10 @@ impl QueryServer { migrate_txn.migrate_4_to_5()?; } + if system_info_version < 6 { + migrate_txn.migrate_5_to_6()?; + } + migrate_txn.commit()?; // Migrations complete. Init idm will now set the version as needed. @@ -1972,6 +1989,18 @@ impl<'a> QueryServerWriteTransaction<'a> { }) } + /// Migrate 5 to 6 - This updates the domain info item to reset the token + /// keys based on the new encryption types. + pub fn migrate_5_to_6(&self) -> Result<(), OperationError> { + spanned!("server::migrate_5_to_6", { + admin_warn!("starting 5 to 6 migration."); + let filter = filter!(f_eq("uuid", (*PVUUID_DOMAIN_INFO).clone())); + let modlist = ModifyList::new_purge("domain_token_key"); + self.internal_modify(&filter, &modlist) + // Complete + }) + } + // These are where searches and other actions are actually implemented. This // is the "internal" version, where we define the event as being internal // only, allowing certain plugin by passes etc. @@ -2249,6 +2278,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_ATTR_DOMAIN_UUID, JSON_SCHEMA_ATTR_DOMAIN_SSID, JSON_SCHEMA_ATTR_DOMAIN_TOKEN_KEY, + JSON_SCHEMA_ATTR_FERNET_PRIVATE_KEY_STR, JSON_SCHEMA_ATTR_GIDNUMBER, JSON_SCHEMA_ATTR_BADLIST_PASSWORD, JSON_SCHEMA_ATTR_LOGINSHELL, diff --git a/kanidmd/src/lib/value.rs b/kanidmd/src/lib/value.rs index ceaf9c9cd..5cd46a634 100644 --- a/kanidmd/src/lib/value.rs +++ b/kanidmd/src/lib/value.rs @@ -1179,6 +1179,12 @@ impl Value { matches!(&self, Value::OauthScopeMap(_, _)) } + #[cfg(test)] + pub fn new_privatebinary_base64(der: &str) -> Self { + let der = base64::decode(der).unwrap(); + Value::PrivateBinary(der) + } + pub fn new_privatebinary(der: &[u8]) -> Self { Value::PrivateBinary(der.to_owned()) }