mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-29 04:13:54 +02:00
Compare commits
11 commits
0dbfcc3db2
...
804b5f997a
Author | SHA1 | Date | |
---|---|---|---|
|
804b5f997a | ||
|
0e0e8ff844 | ||
|
266dc77536 | ||
|
3edee485dd | ||
|
48f7324080 | ||
|
791a182767 | ||
|
51a1e815b2 | ||
|
399e1d71b8 | ||
|
2c53ae77c5 | ||
|
3a3d3eb807 | ||
|
6184d645d2 |
|
@ -10,3 +10,4 @@ kanidmd/sampledata
|
|||
Makefile
|
||||
target
|
||||
test.db
|
||||
Dockerfile
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
## Author
|
||||
|
||||
- William Brown (Firstyear): william@blackhats.net.au
|
||||
- William Brown (Firstyear): <william@blackhats.net.au>
|
||||
|
||||
## Contributors
|
||||
|
||||
|
@ -44,6 +44,7 @@
|
|||
- adamcstephens
|
||||
- Chris Olstrom (colstrom)
|
||||
- Christopher-Robin (cebbinghaus)
|
||||
- Krzysztof Dajka (alteriks)
|
||||
- Fabian Kammel (datosh)
|
||||
- Andris Raugulis (arthepsy)
|
||||
- Jason (argonaut0)
|
||||
|
|
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -2978,10 +2978,12 @@ dependencies = [
|
|||
"hex",
|
||||
"kanidm-hsm-crypto",
|
||||
"kanidm_proto",
|
||||
"md-5",
|
||||
"openssl",
|
||||
"openssl-sys",
|
||||
"rand",
|
||||
"serde",
|
||||
"sha-crypt",
|
||||
"sha2",
|
||||
"sketching",
|
||||
"tracing",
|
||||
|
@ -3528,6 +3530,16 @@ dependencies = [
|
|||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
|
|
|
@ -204,6 +204,7 @@ libsqlite3-sys = "^0.25.2"
|
|||
lodepng = "3.11.0"
|
||||
lru = "^0.12.5"
|
||||
mathru = "^0.13.0"
|
||||
md-5 = "0.10.6"
|
||||
mimalloc = "0.1.43"
|
||||
notify-debouncer-full = { version = "0.1" }
|
||||
num_enum = "^0.5.11"
|
||||
|
|
|
@ -85,7 +85,7 @@ URL **(recommended)**
|
|||
|
||||
<dt>
|
||||
|
||||
[WebFinger URL **(discouraged)**](#webfinger)
|
||||
[WebFinger URL](#webfinger) **(discouraged)**
|
||||
|
||||
</dt>
|
||||
|
||||
|
@ -162,7 +162,7 @@ Token endpoint
|
|||
|
||||
<dt>
|
||||
|
||||
OpenID Connect issuer URI
|
||||
OpenID Connect Issuer URL
|
||||
|
||||
</dt>
|
||||
|
||||
|
@ -458,58 +458,83 @@ Each client has unique signing keys and access secrets, so this is limited to ea
|
|||
|
||||
## WebFinger
|
||||
|
||||
[WebFinger](https://datatracker.ietf.org/doc/html/rfc7033) provides a mechanism
|
||||
for discovering information about people or other entities. It can be used by an
|
||||
identity provider to supply OpenID Connect discovery information.
|
||||
[WebFinger][webfinger] provides a mechanism for discovering information about
|
||||
entities at a well-known URL (`https://{hostname}/.well-known/webfinger`).
|
||||
|
||||
Kanidm provides
|
||||
[an Identity Provider Discovery for OIDC URL](https://datatracker.ietf.org/doc/html/rfc7033#section-3.1)
|
||||
response to all incoming WebFinger requests, using a user's SPN as their account
|
||||
ID. This does not match on email addresses as they are not guaranteed to be
|
||||
unique.
|
||||
It can be used by a WebFinger client to
|
||||
[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.
|
||||
|
||||
However, WebFinger has a number of flaws which make it difficult to use with
|
||||
Kanidm:
|
||||
Kanidm v1.5.1 and later can respond to WebFinger requests, using a user's SPN as
|
||||
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.
|
||||
|
||||
* WebFinger assumes that the identity provider will give the same `iss`
|
||||
(Issuer) for every OAuth 2.0/OIDC client, and there is no standard way for a
|
||||
WebFinger client to report its client ID.
|
||||
When setting up an application to authenticate with Kanidm, WebFinger **does not
|
||||
add any security** over configuring an OIDC Discovery URL directly. In an OIDC
|
||||
context, the specification makes a number of flawed assumptions which make it
|
||||
difficult to use with Kanidm:
|
||||
|
||||
Kanidm uses a *different* `iss` (Issuer) value for each client.
|
||||
* WebFinger assumes that an identity provider will use the same Issuer URL and
|
||||
OIDC Discovery document (which contains endpoint URLs and token signing keys)
|
||||
for *all* OAuth 2.0/OIDC clients.
|
||||
|
||||
* WebFinger requires that this be served at the *root* of the domain of a user's
|
||||
Kanidm uses *client-specific* Issuer URLs, endpoint URLs and token signing
|
||||
keys. This ensures that tokens can only be used with their intended 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`).
|
||||
`https://idm.example.com/.well-known/webfinger?resource=acct%3Auser%40idm.example.com`).
|
||||
|
||||
Kanidm *does not* provide a WebFinger endpoint at its root URL, because it has
|
||||
no way to know *which* OAuth 2.0/OIDC client a WebFinger request is associated
|
||||
with, so could report an incorrect `iss` (Issuer).
|
||||
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.
|
||||
|
||||
You will need a load balancer in front of Kanidm's HTTPS server to redirect
|
||||
requests to the appropriate `/oauth2/openid/:client_id:/.well-known/webfinger`
|
||||
URL. If the client does not follow redirects, you may need to rewrite the
|
||||
request in the load balancer instead.
|
||||
As a result, Kanidm *does not* provide a WebFinger endpoint at its root URL,
|
||||
because it could report an incorrect Issuer URL and lead the client to an
|
||||
incorrect OIDC Discovery document.
|
||||
|
||||
You will need a load balancer in front of Kanidm's HTTPS server to send a HTTP
|
||||
307 redirect to the appropriate
|
||||
`/oauth2/openid/:client_id:/.well-known/webfinger` URL, *while preserving all
|
||||
query parameters*. For example, with Caddy:
|
||||
|
||||
```caddy
|
||||
# Match on a prefix, and use {uri} to preserve all query parameters.
|
||||
# This only supports *one* client.
|
||||
example.com {
|
||||
redir /.well-known/webfinger https://idm.example.com/oauth2/openid/:client_id:{uri} 307
|
||||
}
|
||||
```
|
||||
|
||||
If you have *multiple* WebFinger clients, it will need to map some other
|
||||
property of the request (such as a source IP address or `User-Agent` header)
|
||||
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),
|
||||
**regardless** of what
|
||||
[`rel` parameter](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.1)
|
||||
was specified.
|
||||
|
||||
This is to work around
|
||||
[a broken client](https://tailscale.com/kb/1240/sso-custom-oidc) which doesn't
|
||||
send a `rel` parameter, but expects an Identity Provider Discovery issuer URL
|
||||
in response.
|
||||
[an Identity Provider Discovery for OIDC URL][webfinger-oidc], **ignoring**
|
||||
[`rel` parameter(s)][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.
|
||||
|
||||
Because of the flaws of the WebFinger specification and the deployment
|
||||
difficulties they introduce, we recommend that applications use OpenID Connect
|
||||
Discovery or OAuth 2.0 Authorisation Server Metadata for client configuration
|
||||
instead of WebFinger.
|
||||
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 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
|
||||
|
|
BIN
examples/media/kanidm_proxmox.png
Normal file
BIN
examples/media/kanidm_proxmox.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 70 KiB |
91
examples/proxmox.md
Normal file
91
examples/proxmox.md
Normal file
|
@ -0,0 +1,91 @@
|
|||
# Proxmox PVE/PBS
|
||||
|
||||
## Helpful links
|
||||
|
||||
- <https://pve.proxmox.com/wiki/User_Management>
|
||||
- <https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_openid>
|
||||
|
||||
## Proxmox OIDC limitation
|
||||
|
||||
As of December 2024, the OIDC implementation in Proxmox supports only authentication.
|
||||
Authorization has to be done manually.
|
||||
Mapping user to specific groups won't work yet (steps 2,3,4).
|
||||
|
||||
Patch for this feature exists, but it hasn't been tested extensively:
|
||||
<https://lore.proxmox.com/pve-devel/20240901165512.687801-1-thomas@atskinner.net/>
|
||||
See also:
|
||||
<https://forum.proxmox.com/threads/openid-connect-default-group.103394/>
|
||||
|
||||
## On Kanidm
|
||||
|
||||
### 1. Create the proxmox resource server and configure the redirect URL
|
||||
|
||||
```bash
|
||||
kanidm system oauth2 create proxmox "proxmox" https://yourproxmox.example.com
|
||||
kanidm system oauth2 add-redirect-url "proxmox" https://yourproxmox.example.com
|
||||
```
|
||||
|
||||
### 2. Create the appropriate group(s)
|
||||
|
||||
```bash
|
||||
kanidm group create proxmox_users --name idm_admin
|
||||
kanidm group create proxmox_admins --name idm_admin
|
||||
```
|
||||
|
||||
### 3. Add the appropriate users to the group
|
||||
|
||||
```bash
|
||||
kanidm group add-members proxmox_users user.name
|
||||
kanidm group add-members proxmox_admins user.name
|
||||
```
|
||||
|
||||
### 4. scope map
|
||||
|
||||
```bash
|
||||
kanidm system oauth2 update-claim-map-join 'proxmox' 'proxmox_role' array
|
||||
kanidm system oauth2 update-claim-map 'proxmox' 'proxmox_role' 'proxmox_admins' 'admin'
|
||||
kanidm system oauth2 update-claim-map 'proxmox' 'proxmox_role' 'proxmox_users' 'user'
|
||||
```
|
||||
|
||||
### 5. Add the scopes
|
||||
|
||||
```bash
|
||||
kanidm system oauth2 update-scope-map proxmox proxmox_users email profile openid
|
||||
```
|
||||
|
||||
### 6. Get the client secret
|
||||
|
||||
```bash
|
||||
kanidm system oauth2 show-basic-secret proxmox
|
||||
```
|
||||
|
||||
Copy the value that is returned.
|
||||
|
||||
## On proxmox server
|
||||
|
||||
### Using WebGUI
|
||||
|
||||
Go to <https://yourproxmox.example.com>
|
||||
Select Datacenter->Realms->Add->OpenID Connect Server
|
||||

|
||||
Issuer URL:
|
||||
|
||||
- <https://idm.example.com:8443/oauth2/openid/proxmox>
|
||||
When kanidm is behind reverse proxy or when using docker port mapping:
|
||||
- <https://idm.example.com/oauth2/openid/proxmox>
|
||||
|
||||
Realm: give some proper name or anything that's meaningful
|
||||
|
||||
Client ID: name given in step 1 (resource server)
|
||||
|
||||
Client Key: secret from step 6
|
||||
|
||||
Autocreate Users: Automatically create users if they do not exist. Users are stored in Proxmox Cluster File System (pmxcfs) - /etc/pve/user.cfg
|
||||
|
||||
### Using CLI
|
||||
|
||||
Login to proxmox node and execute:
|
||||
|
||||
```bash
|
||||
pveum realm add kanidm --type openid --issuer-url https://idm.example.com/oauth2/openid/proxmox --client-id proxmox --client-key="secret from step 6" --username-claim username --scopes="email profile openid" --autocreate
|
||||
```
|
|
@ -33,6 +33,9 @@ tracing = { workspace = true }
|
|||
uuid = { workspace = true }
|
||||
x509-cert = { workspace = true, features = ["pem"] }
|
||||
|
||||
md-5 = { workspace = true }
|
||||
sha-crypt = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
sketching = { workspace = true }
|
||||
|
||||
|
|
99
libs/crypto/src/crypt_md5.rs
Normal file
99
libs/crypto/src/crypt_md5.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use md5::{Digest, Md5};
|
||||
use std::cmp::min;
|
||||
|
||||
/// Maximium salt length.
|
||||
const MD5_MAGIC: &str = "$1$";
|
||||
const MD5_TRANSPOSE: &[u8] = b"\x0c\x06\x00\x0d\x07\x01\x0e\x08\x02\x0f\x09\x03\x05\x0a\x04\x0b";
|
||||
|
||||
const CRYPT_HASH64: &[u8] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
pub fn md5_sha2_hash64_encode(bs: &[u8]) -> String {
|
||||
let ngroups = (bs.len() + 2) / 3;
|
||||
let mut out = String::with_capacity(ngroups * 4);
|
||||
for g in 0..ngroups {
|
||||
let mut g_idx = g * 3;
|
||||
let mut enc = 0u32;
|
||||
for _ in 0..3 {
|
||||
let b = (if g_idx < bs.len() { bs[g_idx] } else { 0 }) as u32;
|
||||
enc >>= 8;
|
||||
enc |= b << 16;
|
||||
g_idx += 1;
|
||||
}
|
||||
for _ in 0..4 {
|
||||
out.push(char::from_u32(CRYPT_HASH64[(enc & 0x3F) as usize] as u32).unwrap_or('!'));
|
||||
enc >>= 6;
|
||||
}
|
||||
}
|
||||
match bs.len() % 3 {
|
||||
1 => {
|
||||
out.pop();
|
||||
out.pop();
|
||||
}
|
||||
2 => {
|
||||
out.pop();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn do_md5_crypt(pass: &[u8], salt: &[u8]) -> Vec<u8> {
|
||||
let mut dgst_b = Md5::new();
|
||||
dgst_b.update(pass);
|
||||
dgst_b.update(salt);
|
||||
dgst_b.update(pass);
|
||||
let mut hash_b = dgst_b.finalize();
|
||||
|
||||
let mut dgst_a = Md5::new();
|
||||
dgst_a.update(pass);
|
||||
dgst_a.update(MD5_MAGIC.as_bytes());
|
||||
dgst_a.update(salt);
|
||||
|
||||
let mut plen = pass.len();
|
||||
while plen > 0 {
|
||||
dgst_a.update(&hash_b[..min(plen, 16)]);
|
||||
if plen < 16 {
|
||||
break;
|
||||
}
|
||||
plen -= 16;
|
||||
}
|
||||
|
||||
plen = pass.len();
|
||||
while plen > 0 {
|
||||
if plen & 1 == 0 {
|
||||
dgst_a.update(&pass[..1])
|
||||
} else {
|
||||
dgst_a.update([0u8])
|
||||
}
|
||||
plen >>= 1;
|
||||
}
|
||||
|
||||
let mut hash_a = dgst_a.finalize();
|
||||
|
||||
for r in 0..1000 {
|
||||
let mut dgst_a = Md5::new();
|
||||
if r % 2 == 1 {
|
||||
dgst_a.update(pass);
|
||||
} else {
|
||||
dgst_a.update(hash_a);
|
||||
}
|
||||
if r % 3 > 0 {
|
||||
dgst_a.update(salt);
|
||||
}
|
||||
if r % 7 > 0 {
|
||||
dgst_a.update(pass);
|
||||
}
|
||||
if r % 2 == 0 {
|
||||
dgst_a.update(pass);
|
||||
} else {
|
||||
dgst_a.update(hash_a);
|
||||
}
|
||||
hash_a = dgst_a.finalize();
|
||||
}
|
||||
|
||||
for (i, &ti) in MD5_TRANSPOSE.iter().enumerate() {
|
||||
hash_b[i] = hash_a[ti as usize];
|
||||
}
|
||||
|
||||
md5_sha2_hash64_encode(&hash_b).into_bytes()
|
||||
}
|
|
@ -11,26 +11,24 @@
|
|||
#![deny(clippy::unreachable)]
|
||||
|
||||
use argon2::{Algorithm, Argon2, Params, PasswordHash, Version};
|
||||
use base64::engine::general_purpose;
|
||||
use base64::engine::GeneralPurpose;
|
||||
use base64::{alphabet, Engine};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use base64::engine::general_purpose;
|
||||
use base64urlsafedata::Base64UrlSafeData;
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use kanidm_hsm_crypto::{HmacKey, Tpm};
|
||||
use kanidm_proto::internal::OperationError;
|
||||
use openssl::error::ErrorStack as OpenSSLErrorStack;
|
||||
use openssl::hash::{self, MessageDigest};
|
||||
use openssl::nid::Nid;
|
||||
use openssl::pkcs5::pbkdf2_hmac;
|
||||
use openssl::sha::{Sha1, Sha256, Sha512};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use kanidm_hsm_crypto::{HmacKey, Tpm};
|
||||
|
||||
mod crypt_md5;
|
||||
pub mod mtls;
|
||||
pub mod prelude;
|
||||
pub mod serialise;
|
||||
|
@ -84,6 +82,7 @@ pub enum CryptoError {
|
|||
Argon2,
|
||||
Argon2Version,
|
||||
Argon2Parameters,
|
||||
Crypt,
|
||||
}
|
||||
|
||||
impl From<OpenSSLErrorStack> for CryptoError {
|
||||
|
@ -137,65 +136,15 @@ pub enum DbPasswordV1 {
|
|||
SHA512(Vec<u8>),
|
||||
SSHA512(Vec<u8>, Vec<u8>),
|
||||
NT_MD4(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum ReplPasswordV1 {
|
||||
TPM_ARGON2ID {
|
||||
m_cost: u32,
|
||||
t_cost: u32,
|
||||
p_cost: u32,
|
||||
version: u32,
|
||||
salt: Base64UrlSafeData,
|
||||
key: Base64UrlSafeData,
|
||||
CRYPT_MD5 {
|
||||
s: Base64UrlSafeData,
|
||||
h: Base64UrlSafeData,
|
||||
},
|
||||
ARGON2ID {
|
||||
m_cost: u32,
|
||||
t_cost: u32,
|
||||
p_cost: u32,
|
||||
version: u32,
|
||||
salt: Base64UrlSafeData,
|
||||
key: Base64UrlSafeData,
|
||||
CRYPT_SHA256 {
|
||||
h: String,
|
||||
},
|
||||
PBKDF2 {
|
||||
cost: usize,
|
||||
salt: Base64UrlSafeData,
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
PBKDF2_SHA1 {
|
||||
cost: usize,
|
||||
salt: Base64UrlSafeData,
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
PBKDF2_SHA512 {
|
||||
cost: usize,
|
||||
salt: Base64UrlSafeData,
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
SHA1 {
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
SSHA1 {
|
||||
salt: Base64UrlSafeData,
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
SHA256 {
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
SSHA256 {
|
||||
salt: Base64UrlSafeData,
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
SHA512 {
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
SSHA512 {
|
||||
salt: Base64UrlSafeData,
|
||||
hash: Base64UrlSafeData,
|
||||
},
|
||||
NT_MD4 {
|
||||
hash: Base64UrlSafeData,
|
||||
CRYPT_SHA512 {
|
||||
h: String,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -214,6 +163,9 @@ impl fmt::Debug for DbPasswordV1 {
|
|||
DbPasswordV1::SHA512(_) => write!(f, "SHA512"),
|
||||
DbPasswordV1::SSHA512(_, _) => write!(f, "SSHA512"),
|
||||
DbPasswordV1::NT_MD4(_) => write!(f, "NT_MD4"),
|
||||
DbPasswordV1::CRYPT_MD5 { .. } => write!(f, "CRYPT_MD5"),
|
||||
DbPasswordV1::CRYPT_SHA256 { .. } => write!(f, "CRYPT_SHA256"),
|
||||
DbPasswordV1::CRYPT_SHA512 { .. } => write!(f, "CRYPT_SHA512"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -436,6 +388,16 @@ enum Kdf {
|
|||
SSHA512(Vec<u8>, Vec<u8>),
|
||||
// hash
|
||||
NT_MD4(Vec<u8>),
|
||||
CRYPT_MD5 {
|
||||
s: Vec<u8>,
|
||||
h: Vec<u8>,
|
||||
},
|
||||
CRYPT_SHA256 {
|
||||
h: String,
|
||||
},
|
||||
CRYPT_SHA512 {
|
||||
h: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
@ -498,78 +460,17 @@ impl TryFrom<DbPasswordV1> for Password {
|
|||
DbPasswordV1::NT_MD4(h) => Ok(Password {
|
||||
material: Kdf::NT_MD4(h),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ReplPasswordV1> for Password {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &ReplPasswordV1) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
ReplPasswordV1::TPM_ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version,
|
||||
salt,
|
||||
key,
|
||||
} => Ok(Password {
|
||||
material: Kdf::TPM_ARGON2ID {
|
||||
m_cost: *m_cost,
|
||||
t_cost: *t_cost,
|
||||
p_cost: *p_cost,
|
||||
version: *version,
|
||||
salt: salt.to_vec(),
|
||||
key: key.to_vec(),
|
||||
DbPasswordV1::CRYPT_MD5 { s, h } => Ok(Password {
|
||||
material: Kdf::CRYPT_MD5 {
|
||||
s: s.into(),
|
||||
h: h.into(),
|
||||
},
|
||||
}),
|
||||
ReplPasswordV1::ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version,
|
||||
salt,
|
||||
key,
|
||||
} => Ok(Password {
|
||||
material: Kdf::ARGON2ID {
|
||||
m_cost: *m_cost,
|
||||
t_cost: *t_cost,
|
||||
p_cost: *p_cost,
|
||||
version: *version,
|
||||
salt: salt.to_vec(),
|
||||
key: key.to_vec(),
|
||||
},
|
||||
DbPasswordV1::CRYPT_SHA256 { h } => Ok(Password {
|
||||
material: Kdf::CRYPT_SHA256 { h },
|
||||
}),
|
||||
ReplPasswordV1::PBKDF2 { cost, salt, hash } => Ok(Password {
|
||||
material: Kdf::PBKDF2(*cost, salt.to_vec(), hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::PBKDF2_SHA1 { cost, salt, hash } => Ok(Password {
|
||||
material: Kdf::PBKDF2_SHA1(*cost, salt.to_vec(), hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::PBKDF2_SHA512 { cost, salt, hash } => Ok(Password {
|
||||
material: Kdf::PBKDF2_SHA512(*cost, salt.to_vec(), hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::SHA1 { hash } => Ok(Password {
|
||||
material: Kdf::SHA1(hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::SSHA1 { salt, hash } => Ok(Password {
|
||||
material: Kdf::SSHA1(salt.to_vec(), hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::SHA256 { hash } => Ok(Password {
|
||||
material: Kdf::SHA256(hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::SSHA256 { salt, hash } => Ok(Password {
|
||||
material: Kdf::SSHA256(salt.to_vec(), hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::SHA512 { hash } => Ok(Password {
|
||||
material: Kdf::SHA512(hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::SSHA512 { salt, hash } => Ok(Password {
|
||||
material: Kdf::SSHA512(salt.to_vec(), hash.to_vec()),
|
||||
}),
|
||||
ReplPasswordV1::NT_MD4 { hash } => Ok(Password {
|
||||
material: Kdf::NT_MD4(hash.to_vec()),
|
||||
DbPasswordV1::CRYPT_SHA512 { h } => Ok(Password {
|
||||
material: Kdf::CRYPT_SHA256 { h },
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -665,6 +566,40 @@ impl TryFrom<&str> for Password {
|
|||
// Test 389ds/openldap formats. Shout outs openldap which sometimes makes these
|
||||
// lowercase.
|
||||
|
||||
if let Some(crypt) = value
|
||||
.strip_prefix("{crypt}")
|
||||
.or_else(|| value.strip_prefix("{CRYPT}"))
|
||||
{
|
||||
if let Some(crypt_md5_phc) = crypt.strip_prefix("$1$") {
|
||||
let (salt, hash) = crypt_md5_phc.split_once('$').ok_or(())?;
|
||||
|
||||
// These are a hash64 format, so leave them as bytes, don't try
|
||||
// to decode.
|
||||
let s = salt.as_bytes().to_vec();
|
||||
let h = hash.as_bytes().to_vec();
|
||||
|
||||
return Ok(Password {
|
||||
material: Kdf::CRYPT_MD5 { s, h },
|
||||
});
|
||||
}
|
||||
|
||||
if crypt.starts_with("$5$") {
|
||||
return Ok(Password {
|
||||
material: Kdf::CRYPT_SHA256 {
|
||||
h: crypt.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if crypt.starts_with("$6$") {
|
||||
return Ok(Password {
|
||||
material: Kdf::CRYPT_SHA512 {
|
||||
h: crypt.to_string(),
|
||||
},
|
||||
});
|
||||
}
|
||||
} // End crypt
|
||||
|
||||
if let Some(ds_ssha1) = value
|
||||
.strip_prefix("{SHA}")
|
||||
.or_else(|| value.strip_prefix("{sha}"))
|
||||
|
@ -1242,6 +1177,20 @@ impl Password {
|
|||
})
|
||||
.map(|chal_key| chal_key.as_ref() == key)
|
||||
}
|
||||
(Kdf::CRYPT_MD5 { s, h }, _) => {
|
||||
let chal_key = crypt_md5::do_md5_crypt(cleartext.as_bytes(), s);
|
||||
Ok(chal_key == *h)
|
||||
}
|
||||
(Kdf::CRYPT_SHA256 { h }, _) => {
|
||||
let is_valid = sha_crypt::sha256_check(cleartext, h.as_str()).is_ok();
|
||||
|
||||
Ok(is_valid)
|
||||
}
|
||||
(Kdf::CRYPT_SHA512 { h }, _) => {
|
||||
let is_valid = sha_crypt::sha512_check(cleartext, h.as_str()).is_ok();
|
||||
|
||||
Ok(is_valid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1293,80 +1242,12 @@ impl Password {
|
|||
Kdf::SHA512(hash) => DbPasswordV1::SHA512(hash.clone()),
|
||||
Kdf::SSHA512(salt, hash) => DbPasswordV1::SSHA512(salt.clone(), hash.clone()),
|
||||
Kdf::NT_MD4(hash) => DbPasswordV1::NT_MD4(hash.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_repl_v1(&self) -> ReplPasswordV1 {
|
||||
match &self.material {
|
||||
Kdf::TPM_ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version,
|
||||
salt,
|
||||
key,
|
||||
} => ReplPasswordV1::TPM_ARGON2ID {
|
||||
m_cost: *m_cost,
|
||||
t_cost: *t_cost,
|
||||
p_cost: *p_cost,
|
||||
version: *version,
|
||||
salt: salt.clone().into(),
|
||||
key: key.clone().into(),
|
||||
},
|
||||
Kdf::ARGON2ID {
|
||||
m_cost,
|
||||
t_cost,
|
||||
p_cost,
|
||||
version,
|
||||
salt,
|
||||
key,
|
||||
} => ReplPasswordV1::ARGON2ID {
|
||||
m_cost: *m_cost,
|
||||
t_cost: *t_cost,
|
||||
p_cost: *p_cost,
|
||||
version: *version,
|
||||
salt: salt.clone().into(),
|
||||
key: key.clone().into(),
|
||||
},
|
||||
Kdf::PBKDF2(cost, salt, hash) => ReplPasswordV1::PBKDF2 {
|
||||
cost: *cost,
|
||||
salt: salt.clone().into(),
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::PBKDF2_SHA1(cost, salt, hash) => ReplPasswordV1::PBKDF2_SHA1 {
|
||||
cost: *cost,
|
||||
salt: salt.clone().into(),
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::PBKDF2_SHA512(cost, salt, hash) => ReplPasswordV1::PBKDF2_SHA512 {
|
||||
cost: *cost,
|
||||
salt: salt.clone().into(),
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::SHA1(hash) => ReplPasswordV1::SHA1 {
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::SSHA1(salt, hash) => ReplPasswordV1::SSHA1 {
|
||||
salt: salt.clone().into(),
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::SHA256(hash) => ReplPasswordV1::SHA256 {
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::SSHA256(salt, hash) => ReplPasswordV1::SSHA256 {
|
||||
salt: salt.clone().into(),
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::SHA512(hash) => ReplPasswordV1::SHA512 {
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::SSHA512(salt, hash) => ReplPasswordV1::SSHA512 {
|
||||
salt: salt.clone().into(),
|
||||
hash: hash.clone().into(),
|
||||
},
|
||||
Kdf::NT_MD4(hash) => ReplPasswordV1::NT_MD4 {
|
||||
hash: hash.clone().into(),
|
||||
Kdf::CRYPT_MD5 { s, h } => DbPasswordV1::CRYPT_MD5 {
|
||||
s: s.clone().into(),
|
||||
h: h.clone().into(),
|
||||
},
|
||||
Kdf::CRYPT_SHA256 { h } => DbPasswordV1::CRYPT_SHA256 { h: h.clone() },
|
||||
Kdf::CRYPT_SHA512 { h } => DbPasswordV1::CRYPT_SHA512 { h: h.clone() },
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1402,7 +1283,10 @@ impl Password {
|
|||
| Kdf::SSHA256(_, _)
|
||||
| Kdf::SHA512(_)
|
||||
| Kdf::SSHA512(_, _)
|
||||
| Kdf::NT_MD4(_) => true,
|
||||
| Kdf::NT_MD4(_)
|
||||
| Kdf::CRYPT_MD5 { .. }
|
||||
| Kdf::CRYPT_SHA256 { .. }
|
||||
| Kdf::CRYPT_SHA512 { .. } => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1660,6 +1544,39 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_crypt_md5() {
|
||||
sketching::test_init();
|
||||
let im_pw = "{crypt}$1$zaRIAsoe$7887GzjDTrst0XbDPpF5m.";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_crypt_sha256() {
|
||||
sketching::test_init();
|
||||
let im_pw = "{crypt}$5$3UzV7Sut8EHCUxlN$41V.jtMQmFAOucqI4ImFV43r.bRLjPlN.hyfoCdmGE2";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_from_crypt_sha512() {
|
||||
sketching::test_init();
|
||||
let im_pw = "{crypt}$6$aXn8azL8DXUyuMvj$9aJJC/KEUwygIpf2MTqjQa.f0MEXNg2cGFc62Fet8XpuDVDedM05CweAlxW6GWxnmHqp14CRf6zU7OQoE/bCu0";
|
||||
let password = "password";
|
||||
let r = Password::try_from(im_pw).expect("Failed to parse");
|
||||
|
||||
assert!(r.requires_upgrade());
|
||||
assert!(r.verify(password).unwrap_or(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_password_argon2id_hsm_bind() {
|
||||
sketching::test_init();
|
||||
|
|
|
@ -54,20 +54,43 @@ RUN --mount=type=cache,id=cargo,target=/cargo \
|
|||
--release; \
|
||||
sccache -s
|
||||
|
||||
# Find and copy dynamically linked libraries using ldd
|
||||
# caveat: this actually partially runs the binary, so it doesn't work for cross-compilation
|
||||
RUN <<EOF
|
||||
mkdir -p /out/libs
|
||||
mkdir -p /out/libs-root
|
||||
ldd /usr/src/kanidm/target/release/kanidmd
|
||||
ldd /usr/src/kanidm/target/release/kanidmd | grep -v 'linux-vdso.so' | awk '{print $(NF-1) " " $1}' | sort -u -k 1,1 | awk '{print "install", "-D", $1, (($2 ~ /^\//) ? "/out/libs-root" $2 : "/out/libs/" $2)}' | xargs -I {} sh -c {}
|
||||
ls -Rla /out/libs
|
||||
ls -Rla /out/libs-root
|
||||
EOF
|
||||
|
||||
# ======================
|
||||
|
||||
FROM repos
|
||||
RUN \
|
||||
--mount=type=cache,id=zypp,target=/var/cache/zypp \
|
||||
zypper install -y \
|
||||
timezone \
|
||||
openssl-3 \
|
||||
sqlite3 \
|
||||
pam
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /usr/src/kanidm/target/release/kanidmd /sbin/
|
||||
COPY --from=builder /usr/src/kanidm/server/core/static /hpkg
|
||||
RUN chmod +x /sbin/kanidmd
|
||||
WORKDIR /
|
||||
|
||||
# Copy root certs for tls into image
|
||||
# You can also mount the certs from the host
|
||||
# --volume /etc/ssl/certs:/etc/ssl/certs:ro
|
||||
COPY --from=repos /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
# Copy our build
|
||||
COPY --from=builder --chmod=0755 /usr/src/kanidm/target/release/kanidmd /sbin/
|
||||
# Web assets
|
||||
COPY --from=builder /usr/src/kanidm/server/core/static /hpkg/
|
||||
|
||||
# Copy fixed-path dynamic libraries to their position
|
||||
COPY --from=builder /out/libs-root/ /
|
||||
COPY --from=builder /out/libs/ /lib/
|
||||
|
||||
# Inform loader where to find libraries
|
||||
# This is necessary because opensuse searches for libraries in /lib64 or /lib depending on the architecture, but we don't know which one we're on.
|
||||
# Alternatively, we could symlink /lib64 to /lib, and /usr/lib64 to /usr/lib, etc.
|
||||
# We could always fix this by invoking the loader on the host (which works in a cross build it seems), but this is easier.
|
||||
# On debian, it always searches for libraries in /lib.
|
||||
ENV LD_LIBRARY_PATH=/lib
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
|
|
Loading…
Reference in a new issue