mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
20240607 2417 piv (#2829)
Add some more ground work for future PIV/x509 authentication.
This commit is contained in:
parent
074646bcf3
commit
bd6d9284c0
90
Cargo.lock
generated
90
Cargo.lock
generated
|
@ -932,6 +932,12 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
|
@ -1260,6 +1266,19 @@ version = "2.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"der_derive",
|
||||
"flagset",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "7.0.0"
|
||||
|
@ -1274,6 +1293,17 @@ dependencies = [
|
|||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der_derive"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
|
@ -1611,6 +1641,12 @@ version = "0.4.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flagset"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.30"
|
||||
|
@ -3156,9 +3192,11 @@ dependencies = [
|
|||
"openssl-sys",
|
||||
"rand",
|
||||
"serde",
|
||||
"sha2",
|
||||
"sketching",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"x509-cert",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4469,6 +4507,15 @@ version = "0.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
@ -5520,6 +5567,16 @@ version = "0.9.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sptr"
|
||||
version = "0.3.2"
|
||||
|
@ -5753,6 +5810,27 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a"
|
||||
dependencies = [
|
||||
"tls_codec_derive",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec_derive"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.38.0"
|
||||
|
@ -6834,6 +6912,18 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x509-cert"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"der",
|
||||
"spki",
|
||||
"tls_codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.13.2"
|
||||
|
|
|
@ -197,6 +197,7 @@ serde = "^1.0.197"
|
|||
serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
|
||||
serde_json = "^1.0.114"
|
||||
serde-wasm-bindgen = "0.5"
|
||||
sha2 = "0.10.8"
|
||||
shellexpand = "^2.1.2"
|
||||
smartstring = "^1.0.1"
|
||||
smolset = "^1.3.1"
|
||||
|
@ -243,6 +244,8 @@ web-sys = "^0.3.69"
|
|||
whoami = "^1.5.1"
|
||||
walkdir = "2"
|
||||
|
||||
x509-cert = "0.2.5"
|
||||
|
||||
yew = "^0.20.0"
|
||||
yew-router = "^0.17.0"
|
||||
zxcvbn = "^2.2.2"
|
||||
|
|
|
@ -5,15 +5,15 @@ db_fs_type = "zfs"
|
|||
db_path = "/tmp/kanidm/kanidm.db"
|
||||
tls_chain = "/tmp/kanidm/chain.pem"
|
||||
tls_key = "/tmp/kanidm/key.pem"
|
||||
# tls_client_ca = "/tmp/kanidm/client_ca"
|
||||
tls_client_ca = "/tmp/kanidm/client_ca"
|
||||
|
||||
# The log level of the server. May be one of info, debug, trace
|
||||
#
|
||||
# NOTE: this is overridden by KANIDM_LOG_LEVEL environment variable
|
||||
# Defaults to "info"
|
||||
#
|
||||
log_level = "info"
|
||||
# log_level = "debug"
|
||||
# log_level = "info"
|
||||
log_level = "debug"
|
||||
# log_level = "trace"
|
||||
|
||||
# otel_grpc_url = "http://localhost:4317"
|
||||
|
|
|
@ -261,4 +261,24 @@ impl KanidmClient {
|
|||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_person_certificate_list(&self, id: &str) -> Result<Vec<Entry>, ClientError> {
|
||||
self.perform_get_request(format!("/v1/person/{}/_certificate", id).as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_person_certificate_create(
|
||||
&self,
|
||||
id: &str,
|
||||
pem_data: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut new_cert = Entry {
|
||||
attrs: BTreeMap::new(),
|
||||
};
|
||||
new_cert
|
||||
.attrs
|
||||
.insert(ATTR_CERTIFICATE.to_string(), vec![pem_data.to_string()]);
|
||||
self.perform_post_request(format!("/v1/person/{}/_certificate", id).as_str(), new_cert)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,11 @@ kanidm-hsm-crypto = { workspace = true }
|
|||
openssl-sys = { workspace = true }
|
||||
openssl = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
x509-cert = { workspace = true, features = ["pem"] }
|
||||
|
||||
[dev-dependencies]
|
||||
sketching = { workspace = true }
|
||||
|
|
|
@ -34,6 +34,12 @@ use kanidm_hsm_crypto::{HmacKey, Tpm};
|
|||
pub mod mtls;
|
||||
pub mod prelude;
|
||||
pub mod serialise;
|
||||
pub mod x509_cert;
|
||||
|
||||
pub use sha2;
|
||||
|
||||
pub type Sha256Digest =
|
||||
sha2::digest::generic_array::GenericArray<u8, sha2::digest::typenum::consts::U32>;
|
||||
|
||||
// NIST 800-63.b salt should be 112 bits -> 14 8u8.
|
||||
const PBKDF2_SALT_LEN: usize = 24;
|
||||
|
|
19
libs/crypto/src/x509_cert.rs
Normal file
19
libs/crypto/src/x509_cert.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use crate::Sha256Digest;
|
||||
|
||||
pub use ::x509_cert::der;
|
||||
pub use ::x509_cert::der::pem;
|
||||
pub use ::x509_cert::Certificate;
|
||||
|
||||
use ::sha2::{Digest, Sha256};
|
||||
|
||||
pub fn x509_public_key_s256(certificate: &Certificate) -> Option<Sha256Digest> {
|
||||
let public_key_bytes = certificate
|
||||
.tbs_certificate
|
||||
.subject_public_key_info
|
||||
.subject_public_key
|
||||
.as_bytes()?;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(public_key_bytes);
|
||||
Some(hasher.finalize())
|
||||
}
|
|
@ -63,6 +63,7 @@ pub const ATTR_ATTRIBUTETYPE: &str = "attributetype";
|
|||
pub const ATTR_AUTH_SESSION_EXPIRY: &str = "authsession_expiry";
|
||||
pub const ATTR_AUTH_PASSWORD_MINIMUM_LENGTH: &str = "auth_password_minimum_length";
|
||||
pub const ATTR_BADLIST_PASSWORD: &str = "badlist_password";
|
||||
pub const ATTR_CERTIFICATE: &str = "certificate";
|
||||
pub const ATTR_CLAIM: &str = "claim";
|
||||
pub const ATTR_CLASS: &str = "class";
|
||||
pub const ATTR_CLASSNAME: &str = "classname";
|
||||
|
@ -159,6 +160,7 @@ pub const ATTR_PRIVILEGE_EXPIRY: &str = "privilege_expiry";
|
|||
pub const ATTR_RADIUS_SECRET: &str = "radius_secret";
|
||||
pub const ATTR_RECYCLED: &str = "recycled";
|
||||
pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
|
||||
pub const ATTR_REFERS: &str = "refers";
|
||||
pub const ATTR_REPLICATED: &str = "replicated";
|
||||
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
|
||||
pub const ATTR_SCOPE: &str = "scope";
|
||||
|
|
|
@ -132,6 +132,10 @@ pub enum OperationError {
|
|||
CU0003WebauthnUserNotVerified,
|
||||
// ValueSet errors
|
||||
VS0001IncomingReplSshPublicKey,
|
||||
VS0002CertificatePublicKeyDigest,
|
||||
VS0003CertificateDerDecode,
|
||||
VS0004CertificatePublicKeyDigest,
|
||||
VS0005CertificatePublicKeyDigest,
|
||||
// Value Errors
|
||||
VL0001ValueSshPublicKeyString,
|
||||
|
||||
|
@ -226,120 +230,124 @@ impl OperationError {
|
|||
/// Return the message associated with the error if there is one.
|
||||
fn message(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
OperationError::SessionExpired => None,
|
||||
OperationError::EmptyRequest => None,
|
||||
OperationError::Backend => None,
|
||||
OperationError::NoMatchingEntries => None,
|
||||
OperationError::NoMatchingAttributes => None,
|
||||
OperationError::CorruptedEntry(_) => None,
|
||||
OperationError::CorruptedIndex(_) => None,
|
||||
OperationError::ConsistencyError(_) => None,
|
||||
OperationError::SchemaViolation(_) => None,
|
||||
OperationError::Plugin(_) => None,
|
||||
OperationError::FilterGeneration => None,
|
||||
OperationError::FilterUuidResolution => None,
|
||||
OperationError::InvalidAttributeName(_) => None,
|
||||
OperationError::InvalidAttribute(_) => None,
|
||||
OperationError::InvalidDbState => None,
|
||||
OperationError::InvalidCacheState => None,
|
||||
OperationError::InvalidValueState => None,
|
||||
OperationError::InvalidEntryId => None,
|
||||
OperationError::InvalidRequestState => None,
|
||||
OperationError::InvalidSyncState => None,
|
||||
OperationError::InvalidState => None,
|
||||
OperationError::InvalidEntryState => None,
|
||||
OperationError::InvalidUuid => None,
|
||||
OperationError::InvalidReplChangeId => None,
|
||||
OperationError::InvalidAcpState(_) => None,
|
||||
OperationError::InvalidSchemaState(_) => None,
|
||||
OperationError::InvalidAccountState(_) => None,
|
||||
OperationError::MissingEntries => None,
|
||||
OperationError::ModifyAssertionFailed => None,
|
||||
OperationError::BackendEngine => None,
|
||||
OperationError::SqliteError => None,
|
||||
OperationError::FsError => None,
|
||||
OperationError::SerdeJsonError => None,
|
||||
OperationError::SerdeCborError => None,
|
||||
OperationError::AccessDenied => None,
|
||||
OperationError::NotAuthenticated => None,
|
||||
OperationError::NotAuthorised => None,
|
||||
OperationError::InvalidAuthState(_) => None,
|
||||
OperationError::InvalidSessionState => None,
|
||||
OperationError::SystemProtectedObject => None,
|
||||
OperationError::SystemProtectedAttribute => None,
|
||||
OperationError::PasswordQuality(_) => None,
|
||||
OperationError::CryptographyError => None,
|
||||
OperationError::ResourceLimit => None,
|
||||
OperationError::QueueDisconnected => None,
|
||||
OperationError::Webauthn => None,
|
||||
OperationError::Wait(_) => None,
|
||||
OperationError::ReplReplayFailure => None,
|
||||
OperationError::ReplEntryNotChanged => None,
|
||||
OperationError::ReplInvalidRUVState => None,
|
||||
OperationError::ReplDomainLevelUnsatisfiable => None,
|
||||
OperationError::ReplDomainUuidMismatch => None,
|
||||
OperationError::ReplServerUuidSplitDataState => None,
|
||||
OperationError::TransactionAlreadyCommitted => None,
|
||||
OperationError::ValueDenyName => None,
|
||||
OperationError::CU0002WebauthnRegistrationError => None,
|
||||
OperationError::CU0003WebauthnUserNotVerified => Some("User Verification bit not set while registering credential, you may need to configure a PIN on this device."),
|
||||
OperationError::CU0001WebauthnAttestationNotTrusted => None,
|
||||
OperationError::VS0001IncomingReplSshPublicKey => None,
|
||||
OperationError::VL0001ValueSshPublicKeyString => None,
|
||||
OperationError::SC0001IncomingSshPublicKey => None,
|
||||
OperationError::MG0001InvalidReMigrationLevel => None,
|
||||
OperationError::MG0002RaiseDomainLevelExceedsMaximum => None,
|
||||
OperationError::MG0003ServerPhaseInvalidForMigration => None,
|
||||
OperationError::DB0001MismatchedRestoreVersion => None,
|
||||
OperationError::DB0002MismatchedRestoreVersion => None,
|
||||
OperationError::MG0004DomainLevelInDevelopment => None,
|
||||
OperationError::MG0005GidConstraintsNotMet => None,
|
||||
OperationError::KP0001KeyProviderNotLoaded => None,
|
||||
OperationError::KP0002KeyProviderInvalidClass => None,
|
||||
OperationError::KP0003KeyProviderInvalidType => None,
|
||||
OperationError::KP0004KeyProviderMissingAttributeName => None,
|
||||
OperationError::KP0005KeyProviderDuplicate => None,
|
||||
OperationError::KP0006KeyObjectJwtEs256Generation => None,
|
||||
OperationError::KP0007KeyProviderDefaultNotAvailable => None,
|
||||
OperationError::KP0008KeyObjectMissingUuid => None,
|
||||
OperationError::KP0009KeyObjectPrivateToDer => None,
|
||||
OperationError::KP0010KeyObjectSignerToVerifier => None,
|
||||
OperationError::KP0011KeyObjectMissingClass => None,
|
||||
OperationError::KP0012KeyObjectMissingProvider => None,
|
||||
OperationError::KP0012KeyProviderNotLoaded => None,
|
||||
OperationError::KP0013KeyObjectJwsEs256DerInvalid => None,
|
||||
OperationError::KP0014KeyObjectSignerToVerifier => None,
|
||||
OperationError::KP0015KeyObjectJwsEs256DerInvalid => None,
|
||||
OperationError::KP0016KeyObjectJwsEs256DerInvalid => None,
|
||||
OperationError::KP0017KeyProviderNoSuchKey => None,
|
||||
OperationError::KP0018KeyProviderNoSuchKey => None,
|
||||
OperationError::KP0019KeyProviderUnsupportedAlgorithm => None,
|
||||
OperationError::KP0020KeyObjectNoActiveSigningKeys => None,
|
||||
OperationError::KP0021KeyObjectJwsEs256Signature => None,
|
||||
OperationError::KP0022KeyObjectJwsNotAssociated => None,
|
||||
OperationError::KP0023KeyObjectJwsKeyRevoked => None,
|
||||
OperationError::KP0024KeyObjectJwsInvalid => None,
|
||||
OperationError::KP0025KeyProviderNotAvailable => None,
|
||||
OperationError::KP0026KeyObjectNoSuchKey => None,
|
||||
OperationError::KP0027KeyObjectPublicToDer => None,
|
||||
OperationError::KP0028KeyObjectImportJwsEs256DerInvalid => None,
|
||||
OperationError::KP0029KeyObjectSignerToVerifier => None,
|
||||
OperationError::KP0030KeyObjectPublicToDer => None,
|
||||
OperationError::KP0031KeyObjectNotFound => None,
|
||||
OperationError::KP0032KeyProviderNoSuchKey => None,
|
||||
OperationError::KP0033KeyProviderNoSuchKey => None,
|
||||
OperationError::KP0034KeyProviderUnsupportedAlgorithm => None,
|
||||
OperationError::KP0035KeyObjectJweA128GCMGeneration => None,
|
||||
OperationError::KP0036KeyObjectPrivateToBytes => None,
|
||||
OperationError::KP0037KeyObjectImportJweA128GCMInvalid => None,
|
||||
OperationError::KP0038KeyObjectImportJweA128GCMInvalid => None,
|
||||
OperationError::KP0039KeyObjectJweNotAssociated => None,
|
||||
OperationError::KP0040KeyObjectJweInvalid => None,
|
||||
OperationError::KP0041KeyObjectJweRevoked => None,
|
||||
OperationError::KP0042KeyObjectNoActiveEncryptionKeys => None,
|
||||
OperationError::KP0043KeyObjectJweA128GCMEncryption => None,
|
||||
OperationError::KP0044KeyObjectJwsPublicJwk => None,
|
||||
OperationError::PL0001GidOverlapsSystemRange => None,
|
||||
Self::SessionExpired => None,
|
||||
Self::EmptyRequest => None,
|
||||
Self::Backend => None,
|
||||
Self::NoMatchingEntries => None,
|
||||
Self::NoMatchingAttributes => None,
|
||||
Self::CorruptedEntry(_) => None,
|
||||
Self::CorruptedIndex(_) => None,
|
||||
Self::ConsistencyError(_) => None,
|
||||
Self::SchemaViolation(_) => None,
|
||||
Self::Plugin(_) => None,
|
||||
Self::FilterGeneration => None,
|
||||
Self::FilterUuidResolution => None,
|
||||
Self::InvalidAttributeName(_) => None,
|
||||
Self::InvalidAttribute(_) => None,
|
||||
Self::InvalidDbState => None,
|
||||
Self::InvalidCacheState => None,
|
||||
Self::InvalidValueState => None,
|
||||
Self::InvalidEntryId => None,
|
||||
Self::InvalidRequestState => None,
|
||||
Self::InvalidSyncState => None,
|
||||
Self::InvalidState => None,
|
||||
Self::InvalidEntryState => None,
|
||||
Self::InvalidUuid => None,
|
||||
Self::InvalidReplChangeId => None,
|
||||
Self::InvalidAcpState(_) => None,
|
||||
Self::InvalidSchemaState(_) => None,
|
||||
Self::InvalidAccountState(_) => None,
|
||||
Self::MissingEntries => None,
|
||||
Self::ModifyAssertionFailed => None,
|
||||
Self::BackendEngine => None,
|
||||
Self::SqliteError => None,
|
||||
Self::FsError => None,
|
||||
Self::SerdeJsonError => None,
|
||||
Self::SerdeCborError => None,
|
||||
Self::AccessDenied => None,
|
||||
Self::NotAuthenticated => None,
|
||||
Self::NotAuthorised => None,
|
||||
Self::InvalidAuthState(_) => None,
|
||||
Self::InvalidSessionState => None,
|
||||
Self::SystemProtectedObject => None,
|
||||
Self::SystemProtectedAttribute => None,
|
||||
Self::PasswordQuality(_) => None,
|
||||
Self::CryptographyError => None,
|
||||
Self::ResourceLimit => None,
|
||||
Self::QueueDisconnected => None,
|
||||
Self::Webauthn => None,
|
||||
Self::Wait(_) => None,
|
||||
Self::ReplReplayFailure => None,
|
||||
Self::ReplEntryNotChanged => None,
|
||||
Self::ReplInvalidRUVState => None,
|
||||
Self::ReplDomainLevelUnsatisfiable => None,
|
||||
Self::ReplDomainUuidMismatch => None,
|
||||
Self::ReplServerUuidSplitDataState => None,
|
||||
Self::TransactionAlreadyCommitted => None,
|
||||
Self::ValueDenyName => None,
|
||||
Self::CU0002WebauthnRegistrationError => None,
|
||||
Self::CU0003WebauthnUserNotVerified => Some("User Verification bit not set while registering credential, you may need to configure a PIN on this device."),
|
||||
Self::CU0001WebauthnAttestationNotTrusted => None,
|
||||
Self::VS0001IncomingReplSshPublicKey => None,
|
||||
Self::VS0003CertificateDerDecode => Some("Decoding the stored certificate from DER failed."),
|
||||
Self::VS0002CertificatePublicKeyDigest |
|
||||
Self::VS0004CertificatePublicKeyDigest |
|
||||
Self::VS0005CertificatePublicKeyDigest => Some("The certificates public key is unabled to be digested."),
|
||||
Self::VL0001ValueSshPublicKeyString => None,
|
||||
Self::SC0001IncomingSshPublicKey => None,
|
||||
Self::MG0001InvalidReMigrationLevel => None,
|
||||
Self::MG0002RaiseDomainLevelExceedsMaximum => None,
|
||||
Self::MG0003ServerPhaseInvalidForMigration => None,
|
||||
Self::DB0001MismatchedRestoreVersion => None,
|
||||
Self::DB0002MismatchedRestoreVersion => None,
|
||||
Self::MG0004DomainLevelInDevelopment => None,
|
||||
Self::MG0005GidConstraintsNotMet => None,
|
||||
Self::KP0001KeyProviderNotLoaded => None,
|
||||
Self::KP0002KeyProviderInvalidClass => None,
|
||||
Self::KP0003KeyProviderInvalidType => None,
|
||||
Self::KP0004KeyProviderMissingAttributeName => None,
|
||||
Self::KP0005KeyProviderDuplicate => None,
|
||||
Self::KP0006KeyObjectJwtEs256Generation => None,
|
||||
Self::KP0007KeyProviderDefaultNotAvailable => None,
|
||||
Self::KP0008KeyObjectMissingUuid => None,
|
||||
Self::KP0009KeyObjectPrivateToDer => None,
|
||||
Self::KP0010KeyObjectSignerToVerifier => None,
|
||||
Self::KP0011KeyObjectMissingClass => None,
|
||||
Self::KP0012KeyObjectMissingProvider => None,
|
||||
Self::KP0012KeyProviderNotLoaded => None,
|
||||
Self::KP0013KeyObjectJwsEs256DerInvalid => None,
|
||||
Self::KP0014KeyObjectSignerToVerifier => None,
|
||||
Self::KP0015KeyObjectJwsEs256DerInvalid => None,
|
||||
Self::KP0016KeyObjectJwsEs256DerInvalid => None,
|
||||
Self::KP0017KeyProviderNoSuchKey => None,
|
||||
Self::KP0018KeyProviderNoSuchKey => None,
|
||||
Self::KP0019KeyProviderUnsupportedAlgorithm => None,
|
||||
Self::KP0020KeyObjectNoActiveSigningKeys => None,
|
||||
Self::KP0021KeyObjectJwsEs256Signature => None,
|
||||
Self::KP0022KeyObjectJwsNotAssociated => None,
|
||||
Self::KP0023KeyObjectJwsKeyRevoked => None,
|
||||
Self::KP0024KeyObjectJwsInvalid => None,
|
||||
Self::KP0025KeyProviderNotAvailable => None,
|
||||
Self::KP0026KeyObjectNoSuchKey => None,
|
||||
Self::KP0027KeyObjectPublicToDer => None,
|
||||
Self::KP0028KeyObjectImportJwsEs256DerInvalid => None,
|
||||
Self::KP0029KeyObjectSignerToVerifier => None,
|
||||
Self::KP0030KeyObjectPublicToDer => None,
|
||||
Self::KP0031KeyObjectNotFound => None,
|
||||
Self::KP0032KeyProviderNoSuchKey => None,
|
||||
Self::KP0033KeyProviderNoSuchKey => None,
|
||||
Self::KP0034KeyProviderUnsupportedAlgorithm => None,
|
||||
Self::KP0035KeyObjectJweA128GCMGeneration => None,
|
||||
Self::KP0036KeyObjectPrivateToBytes => None,
|
||||
Self::KP0037KeyObjectImportJweA128GCMInvalid => None,
|
||||
Self::KP0038KeyObjectImportJweA128GCMInvalid => None,
|
||||
Self::KP0039KeyObjectJweNotAssociated => None,
|
||||
Self::KP0040KeyObjectJweInvalid => None,
|
||||
Self::KP0041KeyObjectJweRevoked => None,
|
||||
Self::KP0042KeyObjectNoActiveEncryptionKeys => None,
|
||||
Self::KP0043KeyObjectJweA128GCMEncryption => None,
|
||||
Self::KP0044KeyObjectJwsPublicJwk => None,
|
||||
Self::PL0001GidOverlapsSystemRange => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -465,6 +465,65 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_search_refers(
|
||||
&self,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
filter: Filter<FilterInvalid>,
|
||||
uuid_or_name: String,
|
||||
attrs: Option<Vec<String>>,
|
||||
eventid: Uuid,
|
||||
) -> Result<Vec<ProtoEntry>, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_read = self.idms.proxy_read().await;
|
||||
let ident = idms_prox_read
|
||||
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
let target_uuid = idms_prox_read
|
||||
.qs_read
|
||||
.name_to_uuid(uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!("Error resolving id to target");
|
||||
e
|
||||
})?;
|
||||
|
||||
// Update the filter with the target_uuid
|
||||
let filter = Filter::join_parts_and(
|
||||
filter,
|
||||
filter_all!(f_eq(Attribute::Refers, PartialValue::Refer(target_uuid))),
|
||||
);
|
||||
|
||||
// Make an event from the request
|
||||
let srch = match SearchEvent::from_internal_message(
|
||||
ident,
|
||||
&filter,
|
||||
attrs.as_deref(),
|
||||
&mut idms_prox_read.qs_read,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
admin_error!("Failed to begin internal api search: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
trace!(?srch, "Begin event");
|
||||
|
||||
match idms_prox_read.qs_read.search_ext(&srch) {
|
||||
Ok(entries) => SearchResult::new(&mut idms_prox_read.qs_read, &entries)
|
||||
.map(|ok_sr| ok_sr.into_proto_array()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
|
|
|
@ -97,6 +97,8 @@ impl Modify for SecurityAddon {
|
|||
super::v1::person_id_put_attr,
|
||||
super::v1::person_id_post_attr,
|
||||
super::v1::person_id_delete_attr,
|
||||
super::v1::person_get_id_certificate,
|
||||
super::v1::person_post_id_certificate,
|
||||
super::v1::person_get_id_credential_status,
|
||||
super::v1::person_id_credential_update_get,
|
||||
super::v1::person_id_credential_update_intent_get,
|
||||
|
|
|
@ -39,10 +39,11 @@ use hyper::body::Incoming;
|
|||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID};
|
||||
use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor};
|
||||
use openssl::nid;
|
||||
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod, SslSessionCacheMode, SslVerifyMode};
|
||||
use openssl::x509::X509;
|
||||
|
||||
use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate};
|
||||
|
||||
use sketching::*;
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
|
@ -554,8 +555,8 @@ pub(crate) async fn handle_conn(
|
|||
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||
})?;
|
||||
|
||||
let mut tls_stream = SslStream::new(ssl, stream).map_err(|e| {
|
||||
error!("Failed to create TLS stream: {:?}", e);
|
||||
let mut tls_stream = SslStream::new(ssl, stream).map_err(|err| {
|
||||
error!(?err, "Failed to create TLS stream");
|
||||
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||
})?;
|
||||
|
||||
|
@ -565,25 +566,28 @@ pub(crate) async fn handle_conn(
|
|||
let client_cert = if let Some(peer_cert) = tls_stream.ssl().peer_certificate() {
|
||||
// TODO: This is where we should be checking the CRL!!!
|
||||
|
||||
let subject_key_id = peer_cert
|
||||
.subject_key_id()
|
||||
.map(|ski| ski.as_slice().to_vec());
|
||||
// Extract the cert from openssl to x509-cert which is a better
|
||||
// parser to handle the various extensions.
|
||||
|
||||
let cn = if let Some(cn) = peer_cert
|
||||
.subject_name()
|
||||
.entries_by_nid(nid::Nid::COMMONNAME)
|
||||
.next()
|
||||
{
|
||||
String::from_utf8(cn.data().as_slice().to_vec())
|
||||
.map_err(|err| {
|
||||
warn!(?err, "client certificate CN contains invalid utf-8 - the CN will be ignored!");
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cert_der = peer_cert.to_der().map_err(|ossl_err| {
|
||||
error!(?ossl_err, "unable to process x509 certificate as DER");
|
||||
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||
})?;
|
||||
|
||||
Some(ClientCertInfo { subject_key_id, cn })
|
||||
let certificate = Certificate::from_der(&cert_der).map_err(|ossl_err| {
|
||||
error!(?ossl_err, "unable to process DER certificate to x509");
|
||||
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||
})?;
|
||||
|
||||
let public_key_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
|
||||
error!("subject public key bitstring is not octet aligned");
|
||||
std::io::Error::from(ErrorKind::ConnectionAborted)
|
||||
})?;
|
||||
|
||||
Some(ClientCertInfo {
|
||||
public_key_s256,
|
||||
certificate,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
|
@ -239,6 +239,8 @@ pub async fn json_rest_event_get(
|
|||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
/// Common event handler to search and retrieve entries with a name or id
|
||||
/// and return the result as json proto entries
|
||||
pub async fn json_rest_event_get_id(
|
||||
state: ServerState,
|
||||
id: String,
|
||||
|
@ -258,6 +260,24 @@ pub async fn json_rest_event_get_id(
|
|||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
/// Common event handler to search and retrieve entries that reference another
|
||||
/// entry by the value of name or id and return the result as json proto entries
|
||||
pub async fn json_rest_event_get_refers_id(
|
||||
state: ServerState,
|
||||
refers_id: String,
|
||||
filter: Filter<FilterInvalid>,
|
||||
attrs: Option<Vec<String>>,
|
||||
kopid: KOpId,
|
||||
client_auth_info: ClientAuthInfo,
|
||||
) -> Result<Json<Vec<ProtoEntry>>, WebError> {
|
||||
state
|
||||
.qe_r_ref
|
||||
.handle_search_refers(client_auth_info, filter, refers_id, attrs, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
pub async fn json_rest_event_delete_id(
|
||||
state: ServerState,
|
||||
id: String,
|
||||
|
@ -656,6 +676,59 @@ pub async fn person_id_delete(
|
|||
json_rest_event_delete_id(state, id, filter, kopid, client_auth_info).await
|
||||
}
|
||||
|
||||
// == person -> certificates
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/person/{id}/_certificate",
|
||||
responses(
|
||||
(status=200, body=Option<ProtoEntry>, content_type="application/json"),
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/person/certificate",
|
||||
operation_id = "person_get_id_certificate",
|
||||
)]
|
||||
pub async fn person_get_id_certificate(
|
||||
State(state): State<ServerState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
) -> Result<Json<Vec<ProtoEntry>>, WebError> {
|
||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::ClientCertificate.into()));
|
||||
json_rest_event_get_refers_id(state, id, filter, None, kopid, client_auth_info).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/person/{id}/_certificate",
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
request_body=ProtoEntry,
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/person/certificate",
|
||||
operation_id = "person_post_id_certificate",
|
||||
)]
|
||||
/// Expects the following fields in the attrs field of the req: [certificate]
|
||||
///
|
||||
/// The person's id will be added implicitly as a reference.
|
||||
pub async fn person_post_id_certificate(
|
||||
State(state): State<ServerState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||
Json(mut obj): Json<ProtoEntry>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let classes: Vec<String> = vec![
|
||||
EntryClass::ClientCertificate.into(),
|
||||
EntryClass::Object.into(),
|
||||
];
|
||||
obj.attrs.insert(Attribute::Refers.to_string(), vec![id]);
|
||||
|
||||
json_rest_event_post(state, classes, obj, kopid, client_auth_info).await
|
||||
}
|
||||
|
||||
// // == account ==
|
||||
|
||||
#[utoipa::path(
|
||||
|
@ -3085,16 +3158,14 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
|
|||
.post(person_id_post_attr)
|
||||
.delete(person_id_delete_attr),
|
||||
)
|
||||
// .route("/v1/person/:id/_lock", get(|| async { "TODO" }))
|
||||
// .route("/v1/person/:id/_credential", get(|| async { "TODO" }))
|
||||
.route(
|
||||
"/v1/person/:id/_certificate",
|
||||
get(person_get_id_certificate).post(person_post_id_certificate),
|
||||
)
|
||||
.route(
|
||||
"/v1/person/:id/_credential/_status",
|
||||
get(person_get_id_credential_status),
|
||||
)
|
||||
// .route(
|
||||
// "/v1/person/:id/_credential/:cid/_lock",
|
||||
// get(|| async { "TODO" }),
|
||||
// )
|
||||
.route(
|
||||
"/v1/person/:id/_credential/_update",
|
||||
get(person_id_credential_update_get),
|
||||
|
|
|
@ -620,6 +620,11 @@ pub enum DbValueKeyInternal {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum DbValueCertificate {
|
||||
V1 { certificate_der: Vec<u8> },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueV1 {
|
||||
#[serde(rename = "U8")]
|
||||
|
@ -777,6 +782,8 @@ pub enum DbValueSetV2 {
|
|||
KeyInternal(Vec<DbValueKeyInternal>),
|
||||
#[serde(rename = "HS")]
|
||||
HexString(Vec<String>),
|
||||
#[serde(rename = "X509")]
|
||||
Certificate(Vec<DbValueCertificate>),
|
||||
}
|
||||
|
||||
impl DbValueSetV2 {
|
||||
|
@ -828,6 +835,7 @@ impl DbValueSetV2 {
|
|||
DbValueSetV2::CredentialType(set) => set.len(),
|
||||
DbValueSetV2::WebauthnAttestationCaList { ca_list } => ca_list.len(),
|
||||
DbValueSetV2::KeyInternal(set) => set.len(),
|
||||
DbValueSetV2::Certificate(set) => set.len(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2209,3 +2209,38 @@ lazy_static! {
|
|||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7: BuiltinAcp = BuiltinAcp {
|
||||
classes: vec![
|
||||
EntryClass::Object,
|
||||
EntryClass::AccessControlProfile,
|
||||
EntryClass::AccessControlCreate,
|
||||
EntryClass::AccessControlDelete,
|
||||
EntryClass::AccessControlModify,
|
||||
EntryClass::AccessControlSearch
|
||||
],
|
||||
name: "idm_acp_hp_client_certificate_manager",
|
||||
uuid: UUID_IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER,
|
||||
description: "Builtin IDM Control for allowing client certificate management.",
|
||||
receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_CLIENT_CERTIFICATE_ADMINS]),
|
||||
target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
|
||||
ProtoFilter::Eq(
|
||||
EntryClass::Class.to_string(),
|
||||
EntryClass::ClientCertificate.to_string()
|
||||
),
|
||||
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
|
||||
])),
|
||||
search_attrs: vec![
|
||||
Attribute::Class,
|
||||
Attribute::Uuid,
|
||||
Attribute::Certificate,
|
||||
Attribute::Refers,
|
||||
],
|
||||
modify_removed_attrs: vec![Attribute::Certificate, Attribute::Refers,],
|
||||
modify_present_attrs: vec![Attribute::Certificate, Attribute::Refers,],
|
||||
create_attrs: vec![Attribute::Class, Attribute::Certificate, Attribute::Refers,],
|
||||
create_classes: vec![EntryClass::Object, EntryClass::ClientCertificate,],
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ pub enum Attribute {
|
|||
AuthSessionExpiry,
|
||||
AuthPasswordMinimumLength,
|
||||
BadlistPassword,
|
||||
Certificate,
|
||||
Claim,
|
||||
Class,
|
||||
ClassName,
|
||||
|
@ -153,6 +154,7 @@ pub enum Attribute {
|
|||
PrivilegeExpiry,
|
||||
RadiusSecret,
|
||||
RecycledDirectMemberOf,
|
||||
Refers,
|
||||
Replicated,
|
||||
Rs256PrivateKeyDer,
|
||||
Scope,
|
||||
|
@ -256,6 +258,7 @@ impl TryFrom<String> for Attribute {
|
|||
ATTR_AUTH_SESSION_EXPIRY => Attribute::AuthSessionExpiry,
|
||||
ATTR_AUTH_PASSWORD_MINIMUM_LENGTH => Attribute::AuthPasswordMinimumLength,
|
||||
ATTR_BADLIST_PASSWORD => Attribute::BadlistPassword,
|
||||
ATTR_CERTIFICATE => Attribute::Certificate,
|
||||
ATTR_CLAIM => Attribute::Claim,
|
||||
ATTR_CLASS => Attribute::Class,
|
||||
ATTR_CLASSNAME => Attribute::ClassName,
|
||||
|
@ -351,6 +354,7 @@ impl TryFrom<String> for Attribute {
|
|||
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
|
||||
ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
|
||||
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
|
||||
ATTR_REFERS => Attribute::Refers,
|
||||
ATTR_REPLICATED => Attribute::Replicated,
|
||||
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
|
||||
ATTR_SCOPE => Attribute::Scope,
|
||||
|
@ -429,6 +433,7 @@ impl From<Attribute> for &'static str {
|
|||
Attribute::AuthSessionExpiry => ATTR_AUTH_SESSION_EXPIRY,
|
||||
Attribute::AuthPasswordMinimumLength => ATTR_AUTH_PASSWORD_MINIMUM_LENGTH,
|
||||
Attribute::BadlistPassword => ATTR_BADLIST_PASSWORD,
|
||||
Attribute::Certificate => ATTR_CERTIFICATE,
|
||||
Attribute::Claim => ATTR_CLAIM,
|
||||
Attribute::Class => ATTR_CLASS,
|
||||
Attribute::ClassName => ATTR_CLASSNAME,
|
||||
|
@ -524,6 +529,7 @@ impl From<Attribute> for &'static str {
|
|||
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
|
||||
Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
|
||||
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
|
||||
Attribute::Refers => ATTR_REFERS,
|
||||
Attribute::Replicated => ATTR_REPLICATED,
|
||||
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
|
||||
Attribute::Scope => ATTR_SCOPE,
|
||||
|
@ -624,6 +630,7 @@ pub enum EntryClass {
|
|||
Builtin,
|
||||
Class,
|
||||
ClassType,
|
||||
ClientCertificate,
|
||||
Conflict,
|
||||
DomainInfo,
|
||||
DynGroup,
|
||||
|
@ -677,6 +684,7 @@ impl From<EntryClass> for &'static str {
|
|||
EntryClass::Builtin => ENTRYCLASS_BUILTIN,
|
||||
EntryClass::Class => ATTR_CLASS,
|
||||
EntryClass::ClassType => "classtype",
|
||||
EntryClass::ClientCertificate => "client_certificate",
|
||||
EntryClass::Conflict => "conflict",
|
||||
EntryClass::DomainInfo => "domain_info",
|
||||
EntryClass::DynGroup => ATTR_DYNGROUP,
|
||||
|
|
|
@ -248,6 +248,16 @@ lazy_static! {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
/// Builtin IDM Group for managing client authentication certificates.
|
||||
pub static ref BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7: BuiltinGroup = BuiltinGroup {
|
||||
name: "idm_client_certificate_admins",
|
||||
description: "Builtin Client Certificate Administration Group.",
|
||||
uuid: UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
|
||||
entry_managed_by: Some(UUID_IDM_ADMINS),
|
||||
members: vec![UUID_IDM_ADMINS],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
/// Builtin IDM Group for granting elevated group write and lifecycle permissions.
|
||||
pub static ref IDM_GROUP_ADMINS_V1: BuiltinGroup = BuiltinGroup {
|
||||
name: "idm_group_admins",
|
||||
|
@ -367,6 +377,36 @@ lazy_static! {
|
|||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
/// This must be the last group to init to include the UUID of the other high priv groups.
|
||||
pub static ref IDM_HIGH_PRIVILEGE_DL7: BuiltinGroup = BuiltinGroup {
|
||||
name: "idm_high_privilege",
|
||||
uuid: UUID_IDM_HIGH_PRIVILEGE,
|
||||
entry_managed_by: Some(UUID_IDM_ACCESS_CONTROL_ADMINS),
|
||||
description: "Builtin IDM provided groups with high levels of access that should be audited and limited in modification.",
|
||||
members: vec![
|
||||
UUID_SYSTEM_ADMINS,
|
||||
UUID_IDM_ADMINS,
|
||||
UUID_DOMAIN_ADMINS,
|
||||
UUID_IDM_SERVICE_DESK,
|
||||
UUID_IDM_RECYCLE_BIN_ADMINS,
|
||||
UUID_IDM_SCHEMA_ADMINS,
|
||||
UUID_IDM_ACCESS_CONTROL_ADMINS,
|
||||
UUID_IDM_OAUTH2_ADMINS,
|
||||
UUID_IDM_RADIUS_ADMINS,
|
||||
UUID_IDM_ACCOUNT_POLICY_ADMINS,
|
||||
UUID_IDM_RADIUS_SERVERS,
|
||||
UUID_IDM_GROUP_ADMINS,
|
||||
UUID_IDM_UNIX_ADMINS,
|
||||
UUID_IDM_PEOPLE_PII_READ,
|
||||
UUID_IDM_PEOPLE_ADMINS,
|
||||
UUID_IDM_PEOPLE_ON_BOARDING,
|
||||
UUID_IDM_SERVICE_ACCOUNT_ADMINS,
|
||||
UUID_IDM_CLIENT_CERTIFICATE_ADMINS,
|
||||
UUID_IDM_HIGH_PRIVILEGE,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
/// Make a list of all the non-admin BuiltinGroup's that are created by default, doing it in a standard-ish way so we can use it around the platform
|
||||
|
|
|
@ -706,6 +706,24 @@ pub static ref SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7: SchemaAttribute = Schem
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_ATTR_REFERS_DL7: SchemaAttribute = SchemaAttribute {
|
||||
uuid: UUID_SCHEMA_ATTR_REFERS,
|
||||
name: Attribute::Refers.into(),
|
||||
description: "A reference to linked object".to_string(),
|
||||
multivalue: false,
|
||||
syntax: SyntaxType::ReferenceUuid,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
pub static ref SCHEMA_ATTR_CERTIFICATE_DL7: SchemaAttribute = SchemaAttribute {
|
||||
uuid: UUID_SCHEMA_ATTR_CERTIFICATE,
|
||||
name: Attribute::Certificate.into(),
|
||||
description: "An x509 Certificate".to_string(),
|
||||
multivalue: false,
|
||||
syntax: SyntaxType::Certificate,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// === classes ===
|
||||
|
||||
pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
|
||||
|
@ -1333,4 +1351,18 @@ pub static ref SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6: SchemaClass = SchemaClass {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
// =========================================
|
||||
|
||||
pub static ref SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7: SchemaClass = SchemaClass {
|
||||
uuid: UUID_SCHEMA_CLASS_CLIENT_CERTIFICATE,
|
||||
name: EntryClass::ClientCertificate.into(),
|
||||
description: "A client authentication certificate".to_string(),
|
||||
systemmay: vec![],
|
||||
systemmust: vec![
|
||||
Attribute::Certificate.into(),
|
||||
Attribute::Refers.into(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
);
|
||||
|
|
|
@ -67,6 +67,7 @@ pub const UUID_IDM_PEOPLE_ON_BOARDING: Uuid = uuid!("00000000-0000-0000-0000-000
|
|||
pub const UUID_IDM_SERVICE_ACCOUNT_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000046");
|
||||
pub const UUID_IDM_ACCOUNT_POLICY_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000047");
|
||||
pub const UUID_IDM_PEOPLE_SELF_NAME_WRITE: Uuid = uuid!("00000000-0000-0000-0000-000000000048");
|
||||
pub const UUID_IDM_CLIENT_CERTIFICATE_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000049");
|
||||
|
||||
//
|
||||
pub const UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000");
|
||||
|
@ -304,6 +305,10 @@ pub const UUID_SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM: Uuid =
|
|||
pub const UUID_SCHEMA_ATTR_PATCH_LEVEL: Uuid = uuid!("00000000-0000-0000-0000-ffff00000175");
|
||||
pub const UUID_SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000176");
|
||||
pub const UUID_SCHEMA_ATTR_REFERS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000177");
|
||||
pub const UUID_SCHEMA_ATTR_CERTIFICATE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000178");
|
||||
pub const UUID_SCHEMA_CLASS_CLIENT_CERTIFICATE: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000179");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
@ -416,8 +421,9 @@ pub const UUID_IDM_ACP_HP_GROUP_UNIX_MANAGE_V1: Uuid =
|
|||
uuid!("00000000-0000-0000-0000-ffffff000067");
|
||||
pub const UUID_IDM_ACP_GROUP_UNIX_MANAGE_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000068");
|
||||
pub const UUID_IDM_ACP_ACCOUNT_UNIX_EXTEND_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000069");
|
||||
|
||||
pub const UUID_KEY_PROVIDER_INTERNAL: Uuid = uuid!("00000000-0000-0000-0000-ffffff000070");
|
||||
pub const UUID_IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffffff000071");
|
||||
|
||||
// End of system ranges
|
||||
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");
|
||||
|
|
|
@ -390,6 +390,45 @@ impl Account {
|
|||
})
|
||||
}
|
||||
|
||||
/// Given the currently bound client certificate, yield a user auth token that
|
||||
/// represents the current session for the account.
|
||||
pub(crate) fn client_cert_info_to_userauthtoken(
|
||||
&self,
|
||||
certificate_id: Uuid,
|
||||
session_is_rw: bool,
|
||||
ct: Duration,
|
||||
account_policy: &ResolvedAccountPolicy,
|
||||
) -> Option<UserAuthToken> {
|
||||
let issued_at = OffsetDateTime::UNIX_EPOCH + ct;
|
||||
|
||||
let limit_search_max_results = account_policy.limit_search_max_results();
|
||||
let limit_search_max_filter_test = account_policy.limit_search_max_filter_test();
|
||||
|
||||
let purpose = if session_is_rw {
|
||||
UatPurpose::ReadWrite { expiry: None }
|
||||
} else {
|
||||
UatPurpose::ReadOnly
|
||||
};
|
||||
|
||||
Some(UserAuthToken {
|
||||
session_id: certificate_id,
|
||||
expiry: None,
|
||||
issued_at,
|
||||
purpose,
|
||||
uuid: self.uuid,
|
||||
displayname: self.displayname.clone(),
|
||||
spn: self.spn.clone(),
|
||||
mail_primary: self.mail_primary.clone(),
|
||||
ui_hints: self.ui_hints.clone(),
|
||||
// application: None,
|
||||
// groups: self.groups.iter().map(|g| g.to_proto()).collect(),
|
||||
limit_search_max_results,
|
||||
limit_search_max_filter_test,
|
||||
})
|
||||
}
|
||||
|
||||
/// Determine if an entry is within it's validity period using it's `valid_from` and
|
||||
/// `expire` attributes. `true` indicates the account is within the valid period.
|
||||
pub fn check_within_valid_time(
|
||||
ct: Duration,
|
||||
valid_from: Option<&OffsetDateTime>,
|
||||
|
@ -416,11 +455,14 @@ impl Account {
|
|||
vmin && vmax
|
||||
}
|
||||
|
||||
/// Determine if this account is within it's validity period. `true` indicates the
|
||||
/// account is within the valid period.
|
||||
pub fn is_within_valid_time(&self, ct: Duration) -> bool {
|
||||
Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref())
|
||||
}
|
||||
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
/// Get related inputs, such as account name, email, etc. This is used for password
|
||||
/// quality checking.
|
||||
pub fn related_inputs(&self) -> Vec<&str> {
|
||||
let mut inputs = Vec::with_capacity(4 + self.mail.len());
|
||||
self.mail.iter().for_each(|m| {
|
||||
|
|
|
@ -24,6 +24,7 @@ pub(crate) mod unix;
|
|||
|
||||
use crate::server::identity::Source;
|
||||
use compact_jwt::JwsCompact;
|
||||
use kanidm_lib_crypto::{x509_cert::Certificate, Sha256Digest};
|
||||
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
|
||||
use std::fmt;
|
||||
|
||||
|
@ -55,8 +56,8 @@ pub struct ClientAuthInfo {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientCertInfo {
|
||||
pub subject_key_id: Option<Vec<u8>>,
|
||||
pub cn: Option<String>,
|
||||
pub public_key_s256: Sha256Digest,
|
||||
pub certificate: Certificate,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -367,10 +367,8 @@ pub trait IdmServerTransaction<'a> {
|
|||
} = client_auth_info;
|
||||
|
||||
match (client_cert, bearer_token) {
|
||||
(Some(_client_cert_info), _) => {
|
||||
// TODO: Cert validation here.
|
||||
warn!("Unable to process client certificate identity");
|
||||
Err(OperationError::NotAuthenticated)
|
||||
(Some(client_cert_info), _) => {
|
||||
self.client_certificate_to_identity(&client_cert_info, ct, source)
|
||||
}
|
||||
(None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
|
||||
Token::UserAuthToken(uat) => self.process_uat_to_identity(&uat, ct, source),
|
||||
|
@ -385,6 +383,9 @@ pub trait IdmServerTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// This function is not using in authentication flows - it is a reflector of the
|
||||
/// current session state to allow a user-auth-token to be presented to the
|
||||
/// user via the whoami call.
|
||||
#[instrument(level = "info", skip_all)]
|
||||
fn validate_client_auth_info_to_uat(
|
||||
&mut self,
|
||||
|
@ -399,10 +400,8 @@ pub trait IdmServerTransaction<'a> {
|
|||
} = client_auth_info;
|
||||
|
||||
match (client_cert, bearer_token) {
|
||||
(Some(_client_cert_info), _) => {
|
||||
// TODO: Cert validation here.
|
||||
warn!("Unable to process client certificate identity");
|
||||
Err(OperationError::NotAuthenticated)
|
||||
(Some(client_cert_info), _) => {
|
||||
self.client_certificate_to_user_auth_token(&client_cert_info, ct)
|
||||
}
|
||||
(None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
|
||||
Token::UserAuthToken(uat) => Ok(uat),
|
||||
|
@ -677,6 +676,135 @@ pub trait IdmServerTransaction<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
fn client_cert_info_entry(
|
||||
&mut self,
|
||||
client_cert_info: &ClientCertInfo,
|
||||
) -> Result<Arc<EntrySealedCommitted>, OperationError> {
|
||||
let pks256 = hex::encode(&client_cert_info.public_key_s256);
|
||||
// Using the certificate hash, find our matching cert.
|
||||
let mut maybe_cert_entries = self.get_qs_txn().internal_search(filter!(f_eq(
|
||||
Attribute::Certificate,
|
||||
PartialValue::HexString(pks256.clone())
|
||||
)))?;
|
||||
|
||||
let maybe_cert_entry = maybe_cert_entries.pop();
|
||||
|
||||
if let Some(cert_entry) = maybe_cert_entry {
|
||||
if maybe_cert_entries.is_empty() {
|
||||
Ok(cert_entry)
|
||||
} else {
|
||||
debug!(?pks256, "Multiple certificates matched, unable to proceed.");
|
||||
Err(OperationError::NotAuthenticated)
|
||||
}
|
||||
} else {
|
||||
debug!(?pks256, "No certificates were able to be mapped.");
|
||||
Err(OperationError::NotAuthenticated)
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a certificate, validate it and discover the associated entry that
|
||||
/// the certificate relates to. Currently, this relies on mapping the public
|
||||
/// key sha256 to a stored client certificate, which then links to the owner.
|
||||
///
|
||||
/// In the future we *could* consider alternate mapping strategies such as
|
||||
/// subjectAltName or subject DN, but these have subtle security risks and
|
||||
/// configuration challenges, so binary mapping is the simplest - and safest -
|
||||
/// option today.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
fn client_certificate_to_identity(
|
||||
&mut self,
|
||||
client_cert_info: &ClientCertInfo,
|
||||
ct: Duration,
|
||||
source: Source,
|
||||
) -> Result<Identity, OperationError> {
|
||||
let cert_entry = self.client_cert_info_entry(client_cert_info)?;
|
||||
|
||||
// This is who the certificate belongs to.
|
||||
let refers_uuid = cert_entry
|
||||
.get_ava_single_refer(Attribute::Refers)
|
||||
.ok_or_else(|| {
|
||||
warn!("Invalid certificate entry, missing refers");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
// Now get the related entry.
|
||||
let entry = self.get_qs_txn().internal_search_uuid(refers_uuid)?;
|
||||
|
||||
let (account, account_policy) =
|
||||
Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;
|
||||
|
||||
// Is the account in it's valid window?
|
||||
if !account.is_within_valid_time(ct) {
|
||||
// Nope, expired
|
||||
return Err(OperationError::SessionExpired);
|
||||
};
|
||||
|
||||
// scope is related to the cert. For now, default to RO.
|
||||
let scope = AccessScope::ReadOnly;
|
||||
|
||||
let mut limits = Limits::default();
|
||||
// Apply the limits from the account policy
|
||||
if let Some(lim) = account_policy
|
||||
.limit_search_max_results()
|
||||
.and_then(|v| v.try_into().ok())
|
||||
{
|
||||
limits.search_max_results = lim;
|
||||
}
|
||||
if let Some(lim) = account_policy
|
||||
.limit_search_max_filter_test()
|
||||
.and_then(|v| v.try_into().ok())
|
||||
{
|
||||
limits.search_max_filter_test = lim;
|
||||
}
|
||||
|
||||
let certificate_uuid = cert_entry.get_uuid();
|
||||
|
||||
Ok(Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
source,
|
||||
// session_id is the certificate uuid.
|
||||
session_id: certificate_uuid,
|
||||
scope,
|
||||
limits,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
fn client_certificate_to_user_auth_token(
|
||||
&mut self,
|
||||
client_cert_info: &ClientCertInfo,
|
||||
ct: Duration,
|
||||
) -> Result<UserAuthToken, OperationError> {
|
||||
let cert_entry = self.client_cert_info_entry(client_cert_info)?;
|
||||
|
||||
// This is who the certificate belongs to.
|
||||
let refers_uuid = cert_entry
|
||||
.get_ava_single_refer(Attribute::Refers)
|
||||
.ok_or_else(|| {
|
||||
warn!("Invalid certificate entry, missing refers");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
// Now get the related entry.
|
||||
let entry = self.get_qs_txn().internal_search_uuid(refers_uuid)?;
|
||||
|
||||
let (account, account_policy) =
|
||||
Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;
|
||||
|
||||
// Is the account in it's valid window?
|
||||
if !account.is_within_valid_time(ct) {
|
||||
// Nope, expired
|
||||
return Err(OperationError::SessionExpired);
|
||||
};
|
||||
|
||||
let certificate_uuid = cert_entry.get_uuid();
|
||||
let session_is_rw = false;
|
||||
|
||||
account
|
||||
.client_cert_info_to_userauthtoken(certificate_uuid, session_is_rw, ct, &account_policy)
|
||||
.ok_or(OperationError::InvalidState)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
fn validate_ldap_session(
|
||||
&mut self,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::cid::Cid;
|
||||
use super::entry::EntryChangeState;
|
||||
use super::entry::State;
|
||||
use crate::be::dbvalue::DbValueCertificate;
|
||||
use crate::be::dbvalue::DbValueImage;
|
||||
use crate::be::dbvalue::DbValueKeyInternal;
|
||||
use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
|
||||
|
@ -459,6 +460,9 @@ pub enum ReplAttrV1 {
|
|||
HexString {
|
||||
set: Vec<String>,
|
||||
},
|
||||
Certificate {
|
||||
set: Vec<DbValueCertificate>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
|
|
|
@ -237,8 +237,9 @@ impl SchemaAttribute {
|
|||
SyntaxType::Image => matches!(v, PartialValue::Utf8(_)),
|
||||
SyntaxType::CredentialType => matches!(v, PartialValue::CredentialType(_)),
|
||||
|
||||
SyntaxType::HexString => matches!(v, PartialValue::HexString(_)),
|
||||
SyntaxType::KeyInternal => matches!(v, PartialValue::HexString(_)),
|
||||
SyntaxType::HexString | SyntaxType::Certificate | SyntaxType::KeyInternal => {
|
||||
matches!(v, PartialValue::HexString(_))
|
||||
}
|
||||
|
||||
SyntaxType::WebauthnAttestationCaList => false,
|
||||
};
|
||||
|
@ -303,6 +304,7 @@ impl SchemaAttribute {
|
|||
}
|
||||
SyntaxType::KeyInternal => matches!(v, Value::KeyInternal { .. }),
|
||||
SyntaxType::HexString => matches!(v, Value::HexString(_)),
|
||||
SyntaxType::Certificate => matches!(v, Value::Certificate(_)),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
|
|
|
@ -679,9 +679,12 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
let idm_schema_classes = [
|
||||
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
|
||||
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7.clone().into(),
|
||||
SCHEMA_ATTR_REFERS_DL7.clone().into(),
|
||||
SCHEMA_ATTR_CERTIFICATE_DL7.clone().into(),
|
||||
SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(),
|
||||
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
|
||||
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
|
||||
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
|
||||
];
|
||||
|
||||
idm_schema_classes
|
||||
|
@ -700,6 +703,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
.clone()
|
||||
.try_into()?,
|
||||
IDM_PEOPLE_SELF_MAIL_WRITE_DL7.clone().try_into()?,
|
||||
BUILTIN_GROUP_CLIENT_CERTIFICATE_ADMINS_DL7
|
||||
.clone()
|
||||
.try_into()?,
|
||||
IDM_HIGH_PRIVILEGE_DL7.clone().try_into()?,
|
||||
];
|
||||
|
||||
idm_data
|
||||
|
@ -715,6 +722,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
let idm_data = [
|
||||
IDM_ACP_SELF_WRITE_DL7.clone().into(),
|
||||
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
|
||||
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone().into(),
|
||||
];
|
||||
|
||||
idm_data
|
||||
|
|
|
@ -658,6 +658,8 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute("Internal keys are generated and not able to be set.".to_string())),
|
||||
SyntaxType::HexString => Value::new_hex_string_s(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid hex string syntax".to_string())),
|
||||
SyntaxType::Certificate => Value::new_certificate_s(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid x509 certificate syntax".to_string())),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -781,10 +783,10 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
|
||||
"Invalid - unable to query attestation CA list".to_string(),
|
||||
)),
|
||||
SyntaxType::HexString | SyntaxType::KeyInternal => {
|
||||
SyntaxType::HexString | SyntaxType::KeyInternal | SyntaxType::Certificate => {
|
||||
PartialValue::new_hex_string_s(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute(
|
||||
"Invalid key identifer syntax, expected hex string".to_string(),
|
||||
"Invalid syntax, expected hex string".to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use std::time::Duration;
|
|||
use base64::{engine::general_purpose, Engine as _};
|
||||
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
|
||||
use hashbrown::HashSet;
|
||||
use kanidm_lib_crypto::x509_cert::{der::DecodePem, Certificate};
|
||||
use kanidm_proto::internal::ImageValue;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use openssl::ec::EcKey;
|
||||
|
@ -275,6 +276,7 @@ pub enum SyntaxType {
|
|||
OauthClaimMap = 37,
|
||||
KeyInternal = 38,
|
||||
HexString = 39,
|
||||
Certificate = 40,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -323,6 +325,7 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap),
|
||||
"KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
|
||||
"HEX_STRING" => Ok(SyntaxType::HexString),
|
||||
"CERTIFICATE" => Ok(SyntaxType::Certificate),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -371,6 +374,7 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP",
|
||||
SyntaxType::KeyInternal => "KEY_INTERNAL",
|
||||
SyntaxType::HexString => "HEX_STRING",
|
||||
SyntaxType::Certificate => "CERTIFICATE",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1181,6 +1185,8 @@ pub enum Value {
|
|||
},
|
||||
|
||||
HexString(String),
|
||||
|
||||
Certificate(Certificate),
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
|
@ -1471,6 +1477,10 @@ impl Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_certificate_s(cert_str: &str) -> Option<Self> {
|
||||
Certificate::from_pem(cert_str).map(Value::Certificate).ok()
|
||||
}
|
||||
|
||||
/// Want a `Value::Image`? use this!
|
||||
pub fn new_image(input: &str) -> Result<Self, OperationError> {
|
||||
serde_json::from_str::<ImageValue>(input)
|
||||
|
@ -2008,6 +2018,7 @@ impl Value {
|
|||
|
||||
Value::PhoneNumber(_, _) => true,
|
||||
Value::Address(_) => true,
|
||||
Value::Certificate(_) => true,
|
||||
|
||||
Value::Uuid(_)
|
||||
| Value::Bool(_)
|
||||
|
|
248
server/lib/src/valueset/certificate.rs
Normal file
248
server/lib/src/valueset/certificate.rs
Normal file
|
@ -0,0 +1,248 @@
|
|||
use crate::be::dbvalue::DbValueCertificate;
|
||||
use crate::prelude::*;
|
||||
use crate::repl::proto::ReplAttrV1;
|
||||
use crate::schema::SchemaAttribute;
|
||||
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use kanidm_lib_crypto::{
|
||||
x509_cert::{
|
||||
der::{Decode, Encode, EncodePem},
|
||||
pem::LineEnding,
|
||||
x509_public_key_s256, Certificate,
|
||||
},
|
||||
Sha256Digest,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetCertificate {
|
||||
map: BTreeMap<Sha256Digest, Certificate>,
|
||||
}
|
||||
|
||||
impl ValueSetCertificate {
|
||||
pub fn new(certificate: Certificate) -> Result<Box<Self>, OperationError> {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
let pk_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
|
||||
error!("Unable to digest public key");
|
||||
OperationError::VS0002CertificatePublicKeyDigest
|
||||
})?;
|
||||
map.insert(pk_s256, certificate);
|
||||
|
||||
Ok(Box::new(ValueSetCertificate { map }))
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<DbValueCertificate>) -> Result<ValueSet, OperationError> {
|
||||
Self::from_dbv_iter(data.into_iter())
|
||||
}
|
||||
|
||||
pub fn from_repl_v1(data: &[DbValueCertificate]) -> Result<ValueSet, OperationError> {
|
||||
Self::from_dbv_iter(data.iter().cloned())
|
||||
}
|
||||
|
||||
fn from_dbv_iter(
|
||||
certs: impl Iterator<Item = DbValueCertificate>,
|
||||
) -> Result<ValueSet, OperationError> {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for db_cert in certs {
|
||||
match db_cert {
|
||||
DbValueCertificate::V1 { certificate_der } => {
|
||||
// Parse the DER
|
||||
let certificate =
|
||||
Certificate::from_der(&certificate_der).map_err(|x509_err| {
|
||||
error!(?x509_err, "Unable to restore certificate from DER");
|
||||
OperationError::VS0003CertificateDerDecode
|
||||
})?;
|
||||
|
||||
// sha256 the public key
|
||||
let pk_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
|
||||
error!("Unable to digest public key");
|
||||
OperationError::VS0004CertificatePublicKeyDigest
|
||||
})?;
|
||||
|
||||
map.insert(pk_s256, certificate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Box::new(ValueSetCertificate { map }))
|
||||
}
|
||||
|
||||
fn to_vec_dbvs(&self) -> Vec<DbValueCertificate> {
|
||||
self.map
|
||||
.iter()
|
||||
.filter_map(|(pk_s256, cert)| {
|
||||
cert.to_der()
|
||||
.map_err(|der_err| {
|
||||
error!(
|
||||
?pk_s256,
|
||||
?der_err,
|
||||
"Failed to serialise certificate to der. This value will be dropped!"
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.map(|certificate_der| DbValueCertificate::V1 { certificate_der })
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||
where
|
||||
T: IntoIterator<Item = Certificate>,
|
||||
{
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for certificate in iter {
|
||||
let pk_s256 = x509_public_key_s256(&certificate)?;
|
||||
map.insert(pk_s256, certificate);
|
||||
}
|
||||
|
||||
Some(Box::new(ValueSetCertificate { map }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSetT for ValueSetCertificate {
|
||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||
match value {
|
||||
Value::Certificate(certificate) => {
|
||||
let pk_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
|
||||
error!("Unable to digest public key");
|
||||
OperationError::VS0005CertificatePublicKeyDigest
|
||||
})?;
|
||||
|
||||
// bool -> true if the insert did not trigger a duplicate.
|
||||
Ok(self.map.insert(pk_s256, certificate).is_none())
|
||||
}
|
||||
_ => {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
|
||||
match pv {
|
||||
PartialValue::HexString(hs) => {
|
||||
let mut buf = Sha256Digest::default();
|
||||
if hex::decode_to_slice(&hs, &mut buf).is_ok() {
|
||||
self.map.remove(&buf).is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::HexString(hs) => {
|
||||
let mut buf = Sha256Digest::default();
|
||||
if hex::decode_to_slice(&hs, &mut buf).is_ok() {
|
||||
self.map.contains_key(&buf)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn startswith(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn endswith(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||
self.map.keys().map(hex::encode).collect()
|
||||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::Certificate
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||
Box::new(self.map.iter().filter_map(|(pk_s256, cert)| {
|
||||
cert.to_pem(LineEnding::LF)
|
||||
.ok()
|
||||
.map(|pem| format!("{}\n{}", hex::encode(pk_s256), pem))
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||
let data = self.to_vec_dbvs();
|
||||
DbValueSetV2::Certificate(data)
|
||||
}
|
||||
|
||||
fn to_repl_v1(&self) -> ReplAttrV1 {
|
||||
let set = self.to_vec_dbvs();
|
||||
ReplAttrV1::Certificate { set }
|
||||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
Box::new(
|
||||
self.map
|
||||
.keys()
|
||||
.map(hex::encode)
|
||||
.map(PartialValue::HexString),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||
Box::new(self.map.values().cloned().map(Value::Certificate))
|
||||
}
|
||||
|
||||
fn equal(&self, other: &ValueSet) -> bool {
|
||||
if let Some(other) = other.as_certificate_set() {
|
||||
&self.map == other
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||
if let Some(b) = other.as_certificate_set() {
|
||||
mergemaps!(self.map, b)
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_certificate_single(&self) -> Option<&Certificate> {
|
||||
if self.map.len() == 1 {
|
||||
self.map.values().take(1).next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_certificate_set(&self) -> Option<&BTreeMap<Sha256Digest, Certificate>> {
|
||||
Some(&self.map)
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
|
||||
use dyn_clone::DynClone;
|
||||
use hashbrown::HashSet;
|
||||
use kanidm_lib_crypto::{x509_cert::Certificate, Sha256Digest};
|
||||
use kanidm_proto::internal::ImageValue;
|
||||
use openssl::ec::EcKey;
|
||||
use openssl::pkey::Private;
|
||||
|
@ -28,6 +29,7 @@ pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
|
|||
pub use self::auditlogstring::{ValueSetAuditLogString, AUDIT_LOG_STRING_CAPACITY};
|
||||
pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary};
|
||||
pub use self::bool::ValueSetBool;
|
||||
pub use self::certificate::ValueSetCertificate;
|
||||
pub use self::cid::ValueSetCid;
|
||||
pub use self::cred::{
|
||||
ValueSetAttestedPasskey, ValueSetCredential, ValueSetCredentialType, ValueSetIntentToken,
|
||||
|
@ -64,6 +66,7 @@ mod address;
|
|||
mod auditlogstring;
|
||||
mod binary;
|
||||
mod bool;
|
||||
mod certificate;
|
||||
mod cid;
|
||||
mod cred;
|
||||
mod datetime;
|
||||
|
@ -612,6 +615,16 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
|||
None
|
||||
}
|
||||
|
||||
fn to_certificate_single(&self) -> Option<&Certificate> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn as_certificate_set(&self) -> Option<&BTreeMap<Sha256Digest, Certificate>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn repl_merge_valueset(
|
||||
&self,
|
||||
_older: &ValueSet,
|
||||
|
@ -688,6 +701,7 @@ pub fn from_result_value_iter(
|
|||
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
|
||||
Value::Image(imagevalue) => image::ValueSetImage::new(imagevalue),
|
||||
Value::CredentialType(c) => ValueSetCredentialType::new(c),
|
||||
Value::Certificate(c) => ValueSetCertificate::new(c)?,
|
||||
Value::WebauthnAttestationCaList(_)
|
||||
| Value::PhoneNumber(_, _)
|
||||
| Value::Passkey(_, _, _)
|
||||
|
@ -778,6 +792,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
|||
status_cid,
|
||||
der,
|
||||
} => ValueSetKeyInternal::new(id, usage, valid_from, status, status_cid, der),
|
||||
Value::Certificate(certificate) => ValueSetCertificate::new(certificate)?,
|
||||
|
||||
Value::PhoneNumber(_, _) => {
|
||||
debug_assert!(false);
|
||||
|
@ -842,6 +857,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
|||
DbValueSetV2::OauthClaimMap(set) => ValueSetOauthClaimMap::from_dbvs2(set),
|
||||
DbValueSetV2::KeyInternal(set) => ValueSetKeyInternal::from_dbvs2(set),
|
||||
DbValueSetV2::HexString(set) => ValueSetHexString::from_dbvs2(set),
|
||||
DbValueSetV2::Certificate(set) => ValueSetCertificate::from_dbvs2(set),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -894,5 +910,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
|
|||
ReplAttrV1::OauthClaimMap { set } => ValueSetOauthClaimMap::from_repl_v1(set),
|
||||
ReplAttrV1::KeyInternal { set } => ValueSetKeyInternal::from_repl_v1(set),
|
||||
ReplAttrV1::HexString { set } => ValueSetHexString::from_repl_v1(set),
|
||||
ReplAttrV1::Certificate { set } => ValueSetCertificate::from_repl_v1(set),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ shellexpand = { workspace = true }
|
|||
time = { workspace = true, features = ["serde", "std"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "fs"] }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
uuid = { workspace = true }
|
||||
zxcvbn = { workspace = true }
|
||||
|
|
|
@ -24,8 +24,8 @@ use uuid::Uuid;
|
|||
|
||||
use crate::webauthn::get_authenticator;
|
||||
use crate::{
|
||||
handle_client_error, password_prompt, AccountCredential, AccountRadius, AccountSsh,
|
||||
AccountUserAuthToken, AccountValidity, OutputMode, PersonOpt, PersonPosix,
|
||||
handle_client_error, password_prompt, AccountCertificate, AccountCredential, AccountRadius,
|
||||
AccountSsh, AccountUserAuthToken, AccountValidity, OutputMode, PersonOpt, PersonPosix,
|
||||
};
|
||||
|
||||
impl PersonOpt {
|
||||
|
@ -62,6 +62,10 @@ impl PersonOpt {
|
|||
AccountValidity::ExpireAt(ano) => ano.copt.debug,
|
||||
AccountValidity::BeginFrom(ano) => ano.copt.debug,
|
||||
},
|
||||
PersonOpt::Certificate { commands } => match commands {
|
||||
AccountCertificate::Status { copt, .. }
|
||||
| AccountCertificate::Create { copt, .. } => copt.debug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -497,6 +501,60 @@ impl PersonOpt {
|
|||
}
|
||||
}
|
||||
}, // end PersonOpt::Validity
|
||||
PersonOpt::Certificate { commands } => commands.exec().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountCertificate {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
AccountCertificate::Status { account_id, copt } => {
|
||||
let client = copt.to_client(OpType::Read).await;
|
||||
match client.idm_person_certificate_list(account_id).await {
|
||||
Ok(r) => match copt.output_mode {
|
||||
OutputMode::Json => {
|
||||
let r_attrs: Vec<_> = r.iter().map(|entry| &entry.attrs).collect();
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&r_attrs).expect("Failed to serialise json")
|
||||
);
|
||||
}
|
||||
OutputMode::Text => {
|
||||
if r.is_empty() {
|
||||
println!("No certificates available")
|
||||
} else {
|
||||
r.iter().for_each(|ent| println!("{}", ent))
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => handle_client_error(e, copt.output_mode),
|
||||
}
|
||||
}
|
||||
AccountCertificate::Create {
|
||||
account_id,
|
||||
certificate_path,
|
||||
copt,
|
||||
} => {
|
||||
let pem_data = match tokio::fs::read_to_string(certificate_path).await {
|
||||
Ok(pd) => pd,
|
||||
Err(io_err) => {
|
||||
error!(?io_err, ?certificate_path, "Unable to read PEM data");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let client = copt.to_client(OpType::Write).await;
|
||||
|
||||
if let Err(e) = client
|
||||
.idm_person_certificate_create(&account_id, &pem_data)
|
||||
.await
|
||||
{
|
||||
handle_client_error(e, copt.output_mode);
|
||||
} else {
|
||||
println!("Success");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -531,6 +531,24 @@ pub enum AccountValidity {
|
|||
BeginFrom(AccountNamedValidDateTimeOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AccountCertificate {
|
||||
#[clap(name = "status")]
|
||||
Status {
|
||||
account_id: String,
|
||||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
},
|
||||
#[clap(name = "create")]
|
||||
Create {
|
||||
account_id: String,
|
||||
certificate_path: PathBuf,
|
||||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AccountUserAuthToken {
|
||||
/// Show the status of logged in sessions associated to this account.
|
||||
|
@ -603,6 +621,11 @@ pub enum PersonOpt {
|
|||
#[clap(subcommand)]
|
||||
commands: AccountValidity,
|
||||
},
|
||||
#[clap(name = "certificate", hide = true)]
|
||||
Certificate {
|
||||
#[clap(subcommand)]
|
||||
commands: AccountCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
Loading…
Reference in a new issue