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
This commit is contained in:
Firstyear 2022-03-14 17:29:04 +10:00 committed by GitHub
parent 58fb559262
commit bd41ef8f91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 607 additions and 205 deletions

37
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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.

View file

@ -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).
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).

View file

@ -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

View file

@ -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
}

View file

@ -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"

View file

@ -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::<UserAuthToken>(&token) } {
match jwtu
.validate_embeded()
.map(|jws: Jws<UserAuthToken>| jws.inner)
{
Ok(uat) => {
if time::OffsetDateTime::now_utc() >= uat.expiry {
error!(

View file

@ -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::<UserAuthToken>(&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()
}

View file

@ -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"] }

View file

@ -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" }

View file

@ -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<bundy::hs512::HS512>,
pub jws_signer: std::sync::Arc<JwsSigner>,
pub jws_validator: std::sync::Arc<JwsValidator>,
}
pub trait RequestExtensions {
@ -69,7 +72,7 @@ impl RequestExtensions for tide::Request<AppState> {
fn get_current_auth_session_id(&self) -> Option<Uuid> {
// 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<AppState> {
.and_then(|h| {
// Take the token str and attempt to decrypt
// Attempt to re-inflate a uuid from bytes.
let uat: Option<Uuid> = kref.verify(h.as_str()).ok();
uat
JwsUnverified::from_str(h.as_str()).ok()
})
.and_then(|jwsu| {
jwsu.validate(kref)
.map(|jws: Jws<SessionId>| jws.inner.sessionid)
.ok()
})
// If not there, get from the cookie instead.
.or_else(|| self.session().get::<Uuid>("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);

View file

@ -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<AppState>) -> tide::Result {
let uat = req.get_current_uat();
@ -817,15 +825,25 @@ pub async fn auth(mut req: tide::Request<AppState>) -> 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<AppState>) -> 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))
}

View file

@ -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,

View file

@ -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"

View file

@ -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"]
}
}"#;

View file

@ -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"

View file

@ -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.

View file

@ -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.

View file

@ -427,10 +427,14 @@ impl Entry<EntryInit, EntryNew> {
}).collect();
vs.unwrap()
}
"domain_token_key" => {
"domain_token_key" | "fernet_private_key_str" => {
let vs: Option<ValueSet> = vs.into_iter().map(|v| Value::new_secret_str(&v)).collect();
vs.unwrap()
}
"es256_private_key_der" => {
let vs: Option<ValueSet> = 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<ValueSet> = vs.into_iter().map(|v| Value::new_utf8(v)).collect();

View file

@ -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<DelayedAction>,
webauthn: &Webauthn<WebauthnDomainConfig>,
pw_badlist_set: Option<&HashSet<String>>,
uat_bundy_hmac: &HS512,
uat_jwt_signer: &JwsSigner,
) -> Result<AuthState, OperationError> {
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<String> {
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!(),

View file

@ -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;

View file

@ -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<Mutex<AuthSession>>;
type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
// type CredUpdateSessionMutex = Arc<Mutex<CredUpdateSession>>;
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<Uuid, CredSoftLockMutex>,
/// A set of in progress MFA registrations
mfareg_sessions: BptreeMap<Uuid, MfaRegSession>,
// cred_update_sessions: BptreeMap<Uuid, CredUpdateSessionMutex>,
/// 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<WebauthnDomainConfig>,
pw_badlist_cache: Arc<CowCell<HashSet<String>>>,
oauth2rs: Arc<Oauth2ResourceServers>,
uat_bundy_hmac: Arc<CowCell<HS512>>,
uat_jwt_signer: Arc<CowCell<JwsSigner>>,
uat_jwt_validator: Arc<CowCell<JwsValidator>>,
token_enc_key: Arc<CowCell<Fernet>>,
}
/// 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<DelayedAction>,
webauthn: &'a Webauthn<WebauthnDomainConfig>,
pw_badlist_cache: CowCellReadTxn<HashSet<String>>,
uat_bundy_hmac: CowCellReadTxn<HS512>,
uat_jwt_signer: CowCellReadTxn<JwsSigner>,
uat_jwt_validator: CowCellReadTxn<JwsValidator>,
}
/// 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<HS512>,
uat_jwt_validator: CowCellReadTxn<JwsValidator>,
oauth2rs: Oauth2ResourceServersReadTransaction,
}
@ -134,7 +141,9 @@ pub struct IdmServerProxyWriteTransaction<'a> {
crypto_policy: &'a CryptoPolicy,
webauthn: &'a Webauthn<WebauthnDomainConfig>,
pw_badlist_cache: CowCellWriteTxn<'a, HashSet<String>>,
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<UserAuthToken, OperationError> {
// 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<UserAuthToken>| 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);

View file

@ -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(())

View file

@ -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"]
}
}"#,
);

View file

@ -713,15 +713,28 @@ pub trait QueryServerTransaction<'a> {
})
}
fn get_domain_token_key(&self) -> Result<String, OperationError> {
fn get_domain_fernet_private_key(&self) -> Result<String, OperationError> {
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<Vec<u8>, 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,

View file

@ -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())
}