From bd6d9284c0b7706464b71e6736dedd9a0ee400ef Mon Sep 17 00:00:00 2001 From: Firstyear Date: Tue, 11 Jun 2024 10:54:57 +1000 Subject: [PATCH] 20240607 2417 piv (#2829) Add some more ground work for future PIV/x509 authentication. --- Cargo.lock | 90 +++++++++ Cargo.toml | 3 + examples/insecure_server.toml | 6 +- libs/client/src/person.rs | 20 ++ libs/crypto/Cargo.toml | 2 + libs/crypto/src/lib.rs | 6 + libs/crypto/src/x509_cert.rs | 19 ++ proto/src/constants.rs | 2 + proto/src/internal/error.rs | 236 +++++++++++------------ server/core/src/actors/v1_read.rs | 59 ++++++ server/core/src/https/apidocs/mod.rs | 2 + server/core/src/https/mod.rs | 44 +++-- server/core/src/https/v1.rs | 83 ++++++++- server/lib/src/be/dbvalue.rs | 8 + server/lib/src/constants/acp.rs | 35 ++++ server/lib/src/constants/entries.rs | 8 + server/lib/src/constants/groups.rs | 40 ++++ server/lib/src/constants/schema.rs | 32 ++++ server/lib/src/constants/uuids.rs | 8 +- server/lib/src/idm/account.rs | 44 ++++- server/lib/src/idm/mod.rs | 5 +- server/lib/src/idm/server.rs | 144 +++++++++++++- server/lib/src/repl/proto.rs | 4 + server/lib/src/schema.rs | 6 +- server/lib/src/server/migrations.rs | 8 + server/lib/src/server/mod.rs | 6 +- server/lib/src/value.rs | 11 ++ server/lib/src/valueset/certificate.rs | 248 +++++++++++++++++++++++++ server/lib/src/valueset/mod.rs | 17 ++ tools/cli/Cargo.toml | 2 +- tools/cli/src/cli/person.rs | 62 ++++++- tools/cli/src/opt/kanidm.rs | 23 +++ 32 files changed, 1121 insertions(+), 162 deletions(-) create mode 100644 libs/crypto/src/x509_cert.rs create mode 100644 server/lib/src/valueset/certificate.rs diff --git a/Cargo.lock b/Cargo.lock index e23a5583a..b8ee79af1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 9918aec57..1e3e665d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/insecure_server.toml b/examples/insecure_server.toml index 2a69a7cec..15ce8d782 100644 --- a/examples/insecure_server.toml +++ b/examples/insecure_server.toml @@ -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" diff --git a/libs/client/src/person.rs b/libs/client/src/person.rs index 18878df60..62ed6bb4d 100644 --- a/libs/client/src/person.rs +++ b/libs/client/src/person.rs @@ -261,4 +261,24 @@ impl KanidmClient { ) .await } + + pub async fn idm_person_certificate_list(&self, id: &str) -> Result, 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 + } } diff --git a/libs/crypto/Cargo.toml b/libs/crypto/Cargo.toml index 2150d6e45..53532f9ad 100644 --- a/libs/crypto/Cargo.toml +++ b/libs/crypto/Cargo.toml @@ -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 } diff --git a/libs/crypto/src/lib.rs b/libs/crypto/src/lib.rs index ae1ef38cb..b890e537c 100644 --- a/libs/crypto/src/lib.rs +++ b/libs/crypto/src/lib.rs @@ -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; // NIST 800-63.b salt should be 112 bits -> 14 8u8. const PBKDF2_SALT_LEN: usize = 24; diff --git a/libs/crypto/src/x509_cert.rs b/libs/crypto/src/x509_cert.rs new file mode 100644 index 000000000..7ea371980 --- /dev/null +++ b/libs/crypto/src/x509_cert.rs @@ -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 { + 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()) +} diff --git a/proto/src/constants.rs b/proto/src/constants.rs index 79b85f1bf..d3b477fa8 100644 --- a/proto/src/constants.rs +++ b/proto/src/constants.rs @@ -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"; diff --git a/proto/src/internal/error.rs b/proto/src/internal/error.rs index 29c8b0349..63934e85e 100644 --- a/proto/src/internal/error.rs +++ b/proto/src/internal/error.rs @@ -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, } } } diff --git a/server/core/src/actors/v1_read.rs b/server/core/src/actors/v1_read.rs index b39a3b25c..2a02df833 100644 --- a/server/core/src/actors/v1_read.rs +++ b/server/core/src/actors/v1_read.rs @@ -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, + uuid_or_name: String, + attrs: Option>, + eventid: Uuid, + ) -> Result, 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, diff --git a/server/core/src/https/apidocs/mod.rs b/server/core/src/https/apidocs/mod.rs index d35423022..b635086e4 100644 --- a/server/core/src/https/apidocs/mod.rs +++ b/server/core/src/https/apidocs/mod.rs @@ -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, diff --git a/server/core/src/https/mod.rs b/server/core/src/https/mod.rs index de96237b4..0fa938f2f 100644 --- a/server/core/src/https/mod.rs +++ b/server/core/src/https/mod.rs @@ -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 }; diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs index 4839d3f99..348707af1 100644 --- a/server/core/src/https/v1.rs +++ b/server/core/src/https/v1.rs @@ -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, + attrs: Option>, + kopid: KOpId, + client_auth_info: ClientAuthInfo, +) -> Result>, 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, 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, + Path(id): Path, + Extension(kopid): Extension, + VerifiedClientInformation(client_auth_info): VerifiedClientInformation, +) -> Result>, 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, + Path(id): Path, + Extension(kopid): Extension, + VerifiedClientInformation(client_auth_info): VerifiedClientInformation, + Json(mut obj): Json, +) -> Result, WebError> { + let classes: Vec = 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 { .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), diff --git a/server/lib/src/be/dbvalue.rs b/server/lib/src/be/dbvalue.rs index 08e3e9d32..cc7359562 100644 --- a/server/lib/src/be/dbvalue.rs +++ b/server/lib/src/be/dbvalue.rs @@ -620,6 +620,11 @@ pub enum DbValueKeyInternal { }, } +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum DbValueCertificate { + V1 { certificate_der: Vec }, +} + #[derive(Serialize, Deserialize, Debug)] pub enum DbValueV1 { #[serde(rename = "U8")] @@ -777,6 +782,8 @@ pub enum DbValueSetV2 { KeyInternal(Vec), #[serde(rename = "HS")] HexString(Vec), + #[serde(rename = "X509")] + Certificate(Vec), } 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(), } } diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs index a497b4067..0c053d4c9 100644 --- a/server/lib/src/constants/acp.rs +++ b/server/lib/src/constants/acp.rs @@ -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() + }; +} diff --git a/server/lib/src/constants/entries.rs b/server/lib/src/constants/entries.rs index fb2949bc2..b67ec5bac 100644 --- a/server/lib/src/constants/entries.rs +++ b/server/lib/src/constants/entries.rs @@ -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 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 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 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 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 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, diff --git a/server/lib/src/constants/groups.rs b/server/lib/src/constants/groups.rs index 9761cdc65..bfe0d6aed 100644 --- a/server/lib/src/constants/groups.rs +++ b/server/lib/src/constants/groups.rs @@ -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 diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs index 53eb86e27..69e0719f1 100644 --- a/server/lib/src/constants/schema.rs +++ b/server/lib/src/constants/schema.rs @@ -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() +}; + ); diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index 7b660f735..d093a5bba 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -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"); diff --git a/server/lib/src/idm/account.rs b/server/lib/src/idm/account.rs index 2fcbc163a..e9afc7ac0 100644 --- a/server/lib/src/idm/account.rs +++ b/server/lib/src/idm/account.rs @@ -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 { + 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| { diff --git a/server/lib/src/idm/mod.rs b/server/lib/src/idm/mod.rs index 919849660..5c3a0bc8b 100644 --- a/server/lib/src/idm/mod.rs +++ b/server/lib/src/idm/mod.rs @@ -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>, - pub cn: Option, + pub public_key_s256: Sha256Digest, + pub certificate: Certificate, } #[cfg(test)] diff --git a/server/lib/src/idm/server.rs b/server/lib/src/idm/server.rs index 8219145df..43b8c1ac9 100644 --- a/server/lib/src/idm/server.rs +++ b/server/lib/src/idm/server.rs @@ -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, 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 { + 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 { + 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, diff --git a/server/lib/src/repl/proto.rs b/server/lib/src/repl/proto.rs index 368e9233c..50d420257 100644 --- a/server/lib/src/repl/proto.rs +++ b/server/lib/src/repl/proto.rs @@ -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, }, + Certificate { + set: Vec, + }, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] diff --git a/server/lib/src/schema.rs b/server/lib/src/schema.rs index dcfbf6ff4..7560c1213 100644 --- a/server/lib/src/schema.rs +++ b/server/lib/src/schema.rs @@ -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(()) diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index cb98dfe1b..6796f2d30 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -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 diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 2095a28ee..768bc13a1 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -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(), ) }) } diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs index 235e4ecf1..ae2e98f5b 100644 --- a/server/lib/src/value.rs +++ b/server/lib/src/value.rs @@ -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 { + Certificate::from_pem(cert_str).map(Value::Certificate).ok() + } + /// Want a `Value::Image`? use this! pub fn new_image(input: &str) -> Result { serde_json::from_str::(input) @@ -2008,6 +2018,7 @@ impl Value { Value::PhoneNumber(_, _) => true, Value::Address(_) => true, + Value::Certificate(_) => true, Value::Uuid(_) | Value::Bool(_) diff --git a/server/lib/src/valueset/certificate.rs b/server/lib/src/valueset/certificate.rs new file mode 100644 index 000000000..b4bbe2ecb --- /dev/null +++ b/server/lib/src/valueset/certificate.rs @@ -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, +} + +impl ValueSetCertificate { + pub fn new(certificate: Certificate) -> Result, 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) -> Result { + Self::from_dbv_iter(data.into_iter()) + } + + pub fn from_repl_v1(data: &[DbValueCertificate]) -> Result { + Self::from_dbv_iter(data.iter().cloned()) + } + + fn from_dbv_iter( + certs: impl Iterator, + ) -> Result { + 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 { + 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(iter: T) -> Option> + where + T: IntoIterator, + { + 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 { + 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 { + 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 + '_> { + 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 + '_> { + Box::new( + self.map + .keys() + .map(hex::encode) + .map(PartialValue::HexString), + ) + } + + fn to_value_iter(&self) -> Box + '_> { + 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> { + Some(&self.map) + } +} diff --git a/server/lib/src/valueset/mod.rs b/server/lib/src/valueset/mod.rs index 3ee9e9592..1493f2a42 100644 --- a/server/lib/src/valueset/mod.rs +++ b/server/lib/src/valueset/mod.rs @@ -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> { + 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) -> Result 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 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 { 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), } } diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index 76b9e04e1..eacdd83f4 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -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 } diff --git a/tools/cli/src/cli/person.rs b/tools/cli/src/cli/person.rs index f5fa897af..a1438934a 100644 --- a/tools/cli/src/cli/person.rs +++ b/tools/cli/src/cli/person.rs @@ -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"); + }; + } } } } diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index 7786e925a..8de8c4c86 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -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)]