Compare commits

...

7 commits

Author SHA1 Message Date
micolous 2f9537dfb2
Merge 490a6caa18 into f40679cd52 2025-02-20 14:10:35 +05:30
sinavir f40679cd52
Accept invalid certs and fix token_cache_path ()
* Add accept-invalid-certs option for cli
* Fix token_cache_path behavior

---------

Co-authored-by: sinavir <sinavir@sinavir.fr>
2025-02-20 08:07:48 +00:00
Michael Farrell 490a6caa18 use link refs, suggest static serving, reword SPN spiel 2025-02-20 17:19:57 +10:00
Michael Farrell f61bab6631 note non-validity of SPNs as email addresses per rfc 2025-02-20 17:08:04 +10:00
Michael Farrell 036ac23151 fixup url, trim excess content 2025-02-20 17:02:19 +10:00
Michael Farrell 7a825ccc6d feedbacks: Remove (enterprise) Entra-itis, version banner 2025-02-20 16:31:51 +10:00
Firstyear 52824b58f1
Accept lowercase ldap pwd hashes () 2025-02-20 04:34:27 +00:00
5 changed files with 111 additions and 36 deletions
book/src/integrations
libs
client/src
crypto/src
tools/cli/src

View file

@ -458,27 +458,24 @@ Each client has unique signing keys and access secrets, so this is limited to ea
## WebFinger
> [!NOTE]
>
> WebFinger support requires Kanidm v1.5.1 or later.
[WebFinger](https://datatracker.ietf.org/doc/html/rfc7033) provides a mechanism
for discovering information about entities at a well-known URL
(`http://example.com/.well-known/webfinger`).
[WebFinger][webfinger] provides a mechanism for discovering information about
entities at a well-known URL (`https://{hostname}/.well-known/webfinger`).
It can be used by a WebFinger client to
[discover the OIDC issuer URL](https://datatracker.ietf.org/doc/html/rfc7033#section-3.1)
of an identity provider from the hostname alone, and seems to be intended to
support dynamic client registration flows for large public identity providers.
[discover the OIDC issuer URL][webfinger-oidc] of an identity provider from the
hostname alone, and seems to be intended to support dynamic client registration
flows for large public identity providers.
Kanidm v1.5.1 and later can respond to WebFinger requests, using a user's SPN as
the account (eg: `user@idm.example.com`). This *does not* match on email
addresses, because they are not required by Kanidm nor guaranteed to be unique.
part of [an `acct` URI][rfc7565] (eg: `acct:user@idm.example.com`). While SPNs
and `acct` URIs look like email addresses, [as per RFC 7565][rfc7565s4], there
is no guarantee that it is valid for any particular application protocol, unless
an administrator explicitly provides for it.
When setting up an (enterprise) application to authenticate with Kanidm,
WebFinger **does not add any security** over configuring an OpenID Discovery
URL directly. In an OIDC context, the specification makes a number of flawed
assumptions which make it difficult to use with Kanidm:
When setting up an application to authenticate with Kanidm, WebFinger **does not
add any security** over configuring an OpenID Discovery URL directly. In an OIDC
context, the specification makes a number of flawed assumptions which make it
difficult to use with Kanidm:
* WebFinger assumes that the identity provider will give the same `iss`
(issuer) and OpenID Discovery document, including all URLs and signing keys,
@ -486,11 +483,11 @@ assumptions which make it difficult to use with Kanidm:
Kanidm uses *different* `iss` (issuer), signing keys, and some client-specific
endpoint URLs, which ensures that tokens can only be used with their intended
service. *Changing this behaviour would reduce Kanidm's security.*
service.
* WebFinger endpoints must be served at the *root* of the domain of a user's
SPN (ie: information about the user with SPN `user@idm.example.com` is at
`https://idm.example.com/.well-known/webfinger?resource=acct%3Auser%40idm.example.com&rel=...`).
`https://idm.example.com/.well-known/webfinger?resource=acct%3Auser%40idm.example.com`).
Unlike OIDC Discovery, WebFinger clients do not report their OAuth 2.0/OIDC
client ID in the request, so there is no way to tell them apart.
@ -517,19 +514,28 @@ assumptions which make it difficult to use with Kanidm:
to a client ID, and redirect to the appropriate WebFinger URL for that client.
* Kanidm responds to *all* WebFinger queries with
[an Identity Provider Discovery for OIDC URL](https://datatracker.ietf.org/doc/html/rfc7033#section-3.1),
**ignoring** any supplied
[`rel` parameter](https://datatracker.ietf.org/doc/html/rfc7033#section-4.3).
[an Identity Provider Discovery for OIDC URL][webfinger-oidc], **ignoring**
any supplied [`rel` parameter][webfinger-rel].
If you want to use WebFinger in any *other* context on Kanidm's hostname,
you'll need a load balancer in front of Kanidm which matches on some property
of the request.
WebFinger clients *may* omit the `rel=` parameter, so if another service has
relations for an `acct:` entity and a client *does not* supply the `rel=`
parameter, your load balancer will need to merge JSON responses from Kanidm
and the other service(s).
WebFinger clients *may* omit the `rel=` parameter, so if you host another
service with relations for a Kanidm [`acct:` entity][rfc7565s4] and a client
*does not* supply the `rel=` parameter, your load balancer will need to merge
JSON responses from Kanidm and the other service(s).
Because of these issues, we recommend that (enterprise) applications support
*directly* configuring OIDC using a Discovery URL or OAuth 2.0 Authorisation
Server Metadata URL instead of WebFinger.
Because of these issues, we recommend that applications support *directly*
configuring OIDC using a Discovery URL or OAuth 2.0 Authorisation Server
Metadata URL instead of WebFinger.
If a WebFinger client only checks WebFinger once during setup, you may wish to
temporarily serve an appropriate static WebFinger document for that client
instead.
[rfc7565]: https://datatracker.ietf.org/doc/html/rfc7565
[rfc7565s4]: https://datatracker.ietf.org/doc/html/rfc7565#section-4
[webfinger]: https://datatracker.ietf.org/doc/html/rfc7033
[webfinger-oidc]: https://datatracker.ietf.org/doc/html/rfc7033#section-3.1
[webfinger-rel]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.3

View file

@ -94,7 +94,7 @@ pub struct KanidmClientConfigInstance {
pub verify_hostnames: Option<bool>,
/// Whether to verify the Certificate Authority details of the server's TLS certificate, defaults to `true`.
///
/// Environment variable is slightly inverted - `KANIDM_SKIP_HOSTNAME_VERIFICATION`.
/// Environment variable is slightly inverted - `KANIDM_ACCEPT_INVALID_CERTS`.
pub verify_ca: Option<bool>,
/// Optionally you can specify the path of a CA certificate to use for verifying the server, if you're not using one trusted by your system certificate store.
///
@ -453,6 +453,13 @@ impl KanidmClientBuilder {
}
}
pub fn set_token_cache_path(self, token_cache_path: Option<String>) -> Self {
KanidmClientBuilder {
token_cache_path,
..self
}
}
#[allow(clippy::result_unit_err)]
pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result<Self, ClientError> {
//Okay we have a ca to add. Let's read it in and setup.

View file

@ -662,9 +662,13 @@ impl TryFrom<&str> for Password {
});
}
// Test 389ds formats
// Test 389ds/openldap formats. Shout outs openldap which sometimes makes these
// lowercase.
if let Some(ds_ssha1) = value.strip_prefix("{SHA}") {
if let Some(ds_ssha1) = value
.strip_prefix("{SHA}")
.or_else(|| value.strip_prefix("{sha}"))
{
let h = general_purpose::STANDARD.decode(ds_ssha1).map_err(|_| ())?;
if h.len() != DS_SHA1_HASH_LEN {
return Err(());
@ -674,7 +678,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha1) = value.strip_prefix("{SSHA}") {
if let Some(ds_ssha1) = value
.strip_prefix("{SSHA}")
.or_else(|| value.strip_prefix("{ssha}"))
{
let sh = general_purpose::STANDARD.decode(ds_ssha1).map_err(|_| ())?;
let (h, s) = sh.split_at(DS_SHA1_HASH_LEN);
if s.len() != DS_SHA_SALT_LEN {
@ -685,7 +692,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha256) = value.strip_prefix("{SHA256}") {
if let Some(ds_ssha256) = value
.strip_prefix("{SHA256}")
.or_else(|| value.strip_prefix("{sha256}"))
{
let h = general_purpose::STANDARD
.decode(ds_ssha256)
.map_err(|_| ())?;
@ -697,7 +707,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha256) = value.strip_prefix("{SSHA256}") {
if let Some(ds_ssha256) = value
.strip_prefix("{SSHA256}")
.or_else(|| value.strip_prefix("{ssha256}"))
{
let sh = general_purpose::STANDARD
.decode(ds_ssha256)
.map_err(|_| ())?;
@ -710,7 +723,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha512) = value.strip_prefix("{SHA512}") {
if let Some(ds_ssha512) = value
.strip_prefix("{SHA512}")
.or_else(|| value.strip_prefix("{sha512}"))
{
let h = general_purpose::STANDARD
.decode(ds_ssha512)
.map_err(|_| ())?;
@ -722,7 +738,10 @@ impl TryFrom<&str> for Password {
});
}
if let Some(ds_ssha512) = value.strip_prefix("{SSHA512}") {
if let Some(ds_ssha512) = value
.strip_prefix("{SSHA512}")
.or_else(|| value.strip_prefix("{ssha512}"))
{
let sh = general_purpose::STANDARD
.decode(ds_ssha512)
.map_err(|_| ())?;
@ -1441,8 +1460,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha1() {
let im_pw = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha}W6ph5Mm5Pz8GgiULbPgzG37mj9g=";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1451,8 +1474,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha1() {
let im_pw = "{SSHA}EyzbBiP4u4zxOrLpKTORI/RX3HC6TCTJtnVOCQ==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha}EyzbBiP4u4zxOrLpKTORI/RX3HC6TCTJtnVOCQ==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1461,8 +1488,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha256() {
let im_pw = "{SHA256}XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha256}XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1471,8 +1502,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha256() {
let im_pw = "{SSHA256}luYWfFJOZgxySTsJXHgIaCYww4yMpu6yest69j/wO5n5OycuHFV/GQ==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha256}luYWfFJOZgxySTsJXHgIaCYww4yMpu6yest69j/wO5n5OycuHFV/GQ==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1481,8 +1516,12 @@ mod tests {
#[test]
fn test_password_from_ds_sha512() {
let im_pw = "{SHA512}sQnzu7wkTrgkQZF+0G1hi5AI3Qmzvv0bXgc5THBqi7mAsdd4Xll27ASbRt9fEyavWi6m0QP9B8lThf+rDKy8hg==";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{sha512}sQnzu7wkTrgkQZF+0G1hi5AI3Qmzvv0bXgc5THBqi7mAsdd4Xll27ASbRt9fEyavWi6m0QP9B8lThf+rDKy8hg==";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));
@ -1491,8 +1530,12 @@ mod tests {
#[test]
fn test_password_from_ds_ssha512() {
let im_pw = "{SSHA512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM";
let _r = Password::try_from(im_pw).expect("Failed to parse");
let im_pw = "{ssha512}JwrSUHkI7FTAfHRVR6KoFlSN0E3dmaQWARjZ+/UsShYlENOqDtFVU77HJLLrY2MuSp0jve52+pwtdVl2QUAHukQ0XUf5LDtM";
let password = "password";
let r = Password::try_from(im_pw).expect("Failed to parse");
// Known weak, require upgrade.
assert!(r.requires_upgrade());
assert!(r.verify(password).unwrap_or(false));

View file

@ -91,6 +91,18 @@ impl CommonOpt {
false => client_builder,
};
let client_builder = match self.accept_invalid_certs {
true => {
warn!(
"TLS Certificate Verification disabled!!! This can lead to credential and account compromise!!!"
);
client_builder.danger_accept_invalid_certs(true)
}
false => client_builder,
};
let client_builder = client_builder.set_token_cache_path(self.token_cache_path.clone());
client_builder.build().unwrap_or_else(|e| {
error!("Failed to build client instance -- {:?}", e);
std::process::exit(1);

View file

@ -87,6 +87,13 @@ pub struct CommonOpt {
default_value_t = false
)]
skip_hostname_verification: bool,
/// Don't verify CA
#[clap(
long = "accept-invalid-certs",
env = "KANIDM_ACCEPT_INVALID_CERTS",
default_value_t = false
)]
accept_invalid_certs: bool,
/// Path to a file to cache tokens in, defaults to ~/.cache/kanidm_tokens
#[clap(short, long, env = "KANIDM_TOKEN_CACHE_PATH", hide = true, default_value = None,
value_parser = clap::builder::NonEmptyStringValueParser::new())]