mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
58fb559262
commit
bd41ef8f91
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
||||
|
||||
|
|
162
designs/credential-update.rst
Normal file
162
designs/credential-update.rst
Normal 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.
|
||||
|
||||
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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!(),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue