20240607 2417 piv (#2829)

Add some more ground work for future PIV/x509 authentication.
This commit is contained in:
Firstyear 2024-06-11 10:54:57 +10:00 committed by GitHub
parent 074646bcf3
commit bd6d9284c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1121 additions and 162 deletions

90
Cargo.lock generated
View file

@ -932,6 +932,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.16.2" version = "0.16.2"
@ -1260,6 +1266,19 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" 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]] [[package]]
name = "der-parser" name = "der-parser"
version = "7.0.0" version = "7.0.0"
@ -1274,6 +1293,17 @@ dependencies = [
"rusticata-macros", "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]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -1611,6 +1641,12 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flagset"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb3aa5e95cf9aabc17f060cfa0ced7b83f042390760ca53bf09df9968acaa1"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.30" version = "1.0.30"
@ -3156,9 +3192,11 @@ dependencies = [
"openssl-sys", "openssl-sys",
"rand", "rand",
"serde", "serde",
"sha2",
"sketching", "sketching",
"tracing", "tracing",
"uuid", "uuid",
"x509-cert",
] ]
[[package]] [[package]]
@ -4469,6 +4507,15 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -5520,6 +5567,16 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 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]] [[package]]
name = "sptr" name = "sptr"
version = "0.3.2" version = "0.3.2"
@ -5753,6 +5810,27 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 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]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.0" version = "1.38.0"
@ -6834,6 +6912,18 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "x509-parser" name = "x509-parser"
version = "0.13.2" version = "0.13.2"

View file

@ -197,6 +197,7 @@ serde = "^1.0.197"
serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" } serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
serde_json = "^1.0.114" serde_json = "^1.0.114"
serde-wasm-bindgen = "0.5" serde-wasm-bindgen = "0.5"
sha2 = "0.10.8"
shellexpand = "^2.1.2" shellexpand = "^2.1.2"
smartstring = "^1.0.1" smartstring = "^1.0.1"
smolset = "^1.3.1" smolset = "^1.3.1"
@ -243,6 +244,8 @@ web-sys = "^0.3.69"
whoami = "^1.5.1" whoami = "^1.5.1"
walkdir = "2" walkdir = "2"
x509-cert = "0.2.5"
yew = "^0.20.0" yew = "^0.20.0"
yew-router = "^0.17.0" yew-router = "^0.17.0"
zxcvbn = "^2.2.2" zxcvbn = "^2.2.2"

View file

@ -5,15 +5,15 @@ db_fs_type = "zfs"
db_path = "/tmp/kanidm/kanidm.db" db_path = "/tmp/kanidm/kanidm.db"
tls_chain = "/tmp/kanidm/chain.pem" tls_chain = "/tmp/kanidm/chain.pem"
tls_key = "/tmp/kanidm/key.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 # The log level of the server. May be one of info, debug, trace
# #
# NOTE: this is overridden by KANIDM_LOG_LEVEL environment variable # NOTE: this is overridden by KANIDM_LOG_LEVEL environment variable
# Defaults to "info" # Defaults to "info"
# #
log_level = "info" # log_level = "info"
# log_level = "debug" log_level = "debug"
# log_level = "trace" # log_level = "trace"
# otel_grpc_url = "http://localhost:4317" # otel_grpc_url = "http://localhost:4317"

View file

@ -261,4 +261,24 @@ impl KanidmClient {
) )
.await .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
}
} }

View file

@ -27,9 +27,11 @@ kanidm-hsm-crypto = { workspace = true }
openssl-sys = { workspace = true } openssl-sys = { workspace = true }
openssl = { workspace = true } openssl = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
sha2 = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true } tracing = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
x509-cert = { workspace = true, features = ["pem"] }
[dev-dependencies] [dev-dependencies]
sketching = { workspace = true } sketching = { workspace = true }

View file

@ -34,6 +34,12 @@ use kanidm_hsm_crypto::{HmacKey, Tpm};
pub mod mtls; pub mod mtls;
pub mod prelude; pub mod prelude;
pub mod serialise; 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. // NIST 800-63.b salt should be 112 bits -> 14 8u8.
const PBKDF2_SALT_LEN: usize = 24; const PBKDF2_SALT_LEN: usize = 24;

View 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())
}

View file

@ -63,6 +63,7 @@ pub const ATTR_ATTRIBUTETYPE: &str = "attributetype";
pub const ATTR_AUTH_SESSION_EXPIRY: &str = "authsession_expiry"; pub const ATTR_AUTH_SESSION_EXPIRY: &str = "authsession_expiry";
pub const ATTR_AUTH_PASSWORD_MINIMUM_LENGTH: &str = "auth_password_minimum_length"; pub const ATTR_AUTH_PASSWORD_MINIMUM_LENGTH: &str = "auth_password_minimum_length";
pub const ATTR_BADLIST_PASSWORD: &str = "badlist_password"; pub const ATTR_BADLIST_PASSWORD: &str = "badlist_password";
pub const ATTR_CERTIFICATE: &str = "certificate";
pub const ATTR_CLAIM: &str = "claim"; pub const ATTR_CLAIM: &str = "claim";
pub const ATTR_CLASS: &str = "class"; pub const ATTR_CLASS: &str = "class";
pub const ATTR_CLASSNAME: &str = "classname"; 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_RADIUS_SECRET: &str = "radius_secret";
pub const ATTR_RECYCLED: &str = "recycled"; pub const ATTR_RECYCLED: &str = "recycled";
pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof"; pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
pub const ATTR_REFERS: &str = "refers";
pub const ATTR_REPLICATED: &str = "replicated"; pub const ATTR_REPLICATED: &str = "replicated";
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der"; pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
pub const ATTR_SCOPE: &str = "scope"; pub const ATTR_SCOPE: &str = "scope";

View file

@ -132,6 +132,10 @@ pub enum OperationError {
CU0003WebauthnUserNotVerified, CU0003WebauthnUserNotVerified,
// ValueSet errors // ValueSet errors
VS0001IncomingReplSshPublicKey, VS0001IncomingReplSshPublicKey,
VS0002CertificatePublicKeyDigest,
VS0003CertificateDerDecode,
VS0004CertificatePublicKeyDigest,
VS0005CertificatePublicKeyDigest,
// Value Errors // Value Errors
VL0001ValueSshPublicKeyString, VL0001ValueSshPublicKeyString,
@ -226,120 +230,124 @@ impl OperationError {
/// Return the message associated with the error if there is one. /// Return the message associated with the error if there is one.
fn message(&self) -> Option<&'static str> { fn message(&self) -> Option<&'static str> {
match self { match self {
OperationError::SessionExpired => None, Self::SessionExpired => None,
OperationError::EmptyRequest => None, Self::EmptyRequest => None,
OperationError::Backend => None, Self::Backend => None,
OperationError::NoMatchingEntries => None, Self::NoMatchingEntries => None,
OperationError::NoMatchingAttributes => None, Self::NoMatchingAttributes => None,
OperationError::CorruptedEntry(_) => None, Self::CorruptedEntry(_) => None,
OperationError::CorruptedIndex(_) => None, Self::CorruptedIndex(_) => None,
OperationError::ConsistencyError(_) => None, Self::ConsistencyError(_) => None,
OperationError::SchemaViolation(_) => None, Self::SchemaViolation(_) => None,
OperationError::Plugin(_) => None, Self::Plugin(_) => None,
OperationError::FilterGeneration => None, Self::FilterGeneration => None,
OperationError::FilterUuidResolution => None, Self::FilterUuidResolution => None,
OperationError::InvalidAttributeName(_) => None, Self::InvalidAttributeName(_) => None,
OperationError::InvalidAttribute(_) => None, Self::InvalidAttribute(_) => None,
OperationError::InvalidDbState => None, Self::InvalidDbState => None,
OperationError::InvalidCacheState => None, Self::InvalidCacheState => None,
OperationError::InvalidValueState => None, Self::InvalidValueState => None,
OperationError::InvalidEntryId => None, Self::InvalidEntryId => None,
OperationError::InvalidRequestState => None, Self::InvalidRequestState => None,
OperationError::InvalidSyncState => None, Self::InvalidSyncState => None,
OperationError::InvalidState => None, Self::InvalidState => None,
OperationError::InvalidEntryState => None, Self::InvalidEntryState => None,
OperationError::InvalidUuid => None, Self::InvalidUuid => None,
OperationError::InvalidReplChangeId => None, Self::InvalidReplChangeId => None,
OperationError::InvalidAcpState(_) => None, Self::InvalidAcpState(_) => None,
OperationError::InvalidSchemaState(_) => None, Self::InvalidSchemaState(_) => None,
OperationError::InvalidAccountState(_) => None, Self::InvalidAccountState(_) => None,
OperationError::MissingEntries => None, Self::MissingEntries => None,
OperationError::ModifyAssertionFailed => None, Self::ModifyAssertionFailed => None,
OperationError::BackendEngine => None, Self::BackendEngine => None,
OperationError::SqliteError => None, Self::SqliteError => None,
OperationError::FsError => None, Self::FsError => None,
OperationError::SerdeJsonError => None, Self::SerdeJsonError => None,
OperationError::SerdeCborError => None, Self::SerdeCborError => None,
OperationError::AccessDenied => None, Self::AccessDenied => None,
OperationError::NotAuthenticated => None, Self::NotAuthenticated => None,
OperationError::NotAuthorised => None, Self::NotAuthorised => None,
OperationError::InvalidAuthState(_) => None, Self::InvalidAuthState(_) => None,
OperationError::InvalidSessionState => None, Self::InvalidSessionState => None,
OperationError::SystemProtectedObject => None, Self::SystemProtectedObject => None,
OperationError::SystemProtectedAttribute => None, Self::SystemProtectedAttribute => None,
OperationError::PasswordQuality(_) => None, Self::PasswordQuality(_) => None,
OperationError::CryptographyError => None, Self::CryptographyError => None,
OperationError::ResourceLimit => None, Self::ResourceLimit => None,
OperationError::QueueDisconnected => None, Self::QueueDisconnected => None,
OperationError::Webauthn => None, Self::Webauthn => None,
OperationError::Wait(_) => None, Self::Wait(_) => None,
OperationError::ReplReplayFailure => None, Self::ReplReplayFailure => None,
OperationError::ReplEntryNotChanged => None, Self::ReplEntryNotChanged => None,
OperationError::ReplInvalidRUVState => None, Self::ReplInvalidRUVState => None,
OperationError::ReplDomainLevelUnsatisfiable => None, Self::ReplDomainLevelUnsatisfiable => None,
OperationError::ReplDomainUuidMismatch => None, Self::ReplDomainUuidMismatch => None,
OperationError::ReplServerUuidSplitDataState => None, Self::ReplServerUuidSplitDataState => None,
OperationError::TransactionAlreadyCommitted => None, Self::TransactionAlreadyCommitted => None,
OperationError::ValueDenyName => None, Self::ValueDenyName => None,
OperationError::CU0002WebauthnRegistrationError => None, Self::CU0002WebauthnRegistrationError => None,
OperationError::CU0003WebauthnUserNotVerified => Some("User Verification bit not set while registering credential, you may need to configure a PIN on this device."), Self::CU0003WebauthnUserNotVerified => Some("User Verification bit not set while registering credential, you may need to configure a PIN on this device."),
OperationError::CU0001WebauthnAttestationNotTrusted => None, Self::CU0001WebauthnAttestationNotTrusted => None,
OperationError::VS0001IncomingReplSshPublicKey => None, Self::VS0001IncomingReplSshPublicKey => None,
OperationError::VL0001ValueSshPublicKeyString => None, Self::VS0003CertificateDerDecode => Some("Decoding the stored certificate from DER failed."),
OperationError::SC0001IncomingSshPublicKey => None, Self::VS0002CertificatePublicKeyDigest |
OperationError::MG0001InvalidReMigrationLevel => None, Self::VS0004CertificatePublicKeyDigest |
OperationError::MG0002RaiseDomainLevelExceedsMaximum => None, Self::VS0005CertificatePublicKeyDigest => Some("The certificates public key is unabled to be digested."),
OperationError::MG0003ServerPhaseInvalidForMigration => None, Self::VL0001ValueSshPublicKeyString => None,
OperationError::DB0001MismatchedRestoreVersion => None, Self::SC0001IncomingSshPublicKey => None,
OperationError::DB0002MismatchedRestoreVersion => None, Self::MG0001InvalidReMigrationLevel => None,
OperationError::MG0004DomainLevelInDevelopment => None, Self::MG0002RaiseDomainLevelExceedsMaximum => None,
OperationError::MG0005GidConstraintsNotMet => None, Self::MG0003ServerPhaseInvalidForMigration => None,
OperationError::KP0001KeyProviderNotLoaded => None, Self::DB0001MismatchedRestoreVersion => None,
OperationError::KP0002KeyProviderInvalidClass => None, Self::DB0002MismatchedRestoreVersion => None,
OperationError::KP0003KeyProviderInvalidType => None, Self::MG0004DomainLevelInDevelopment => None,
OperationError::KP0004KeyProviderMissingAttributeName => None, Self::MG0005GidConstraintsNotMet => None,
OperationError::KP0005KeyProviderDuplicate => None, Self::KP0001KeyProviderNotLoaded => None,
OperationError::KP0006KeyObjectJwtEs256Generation => None, Self::KP0002KeyProviderInvalidClass => None,
OperationError::KP0007KeyProviderDefaultNotAvailable => None, Self::KP0003KeyProviderInvalidType => None,
OperationError::KP0008KeyObjectMissingUuid => None, Self::KP0004KeyProviderMissingAttributeName => None,
OperationError::KP0009KeyObjectPrivateToDer => None, Self::KP0005KeyProviderDuplicate => None,
OperationError::KP0010KeyObjectSignerToVerifier => None, Self::KP0006KeyObjectJwtEs256Generation => None,
OperationError::KP0011KeyObjectMissingClass => None, Self::KP0007KeyProviderDefaultNotAvailable => None,
OperationError::KP0012KeyObjectMissingProvider => None, Self::KP0008KeyObjectMissingUuid => None,
OperationError::KP0012KeyProviderNotLoaded => None, Self::KP0009KeyObjectPrivateToDer => None,
OperationError::KP0013KeyObjectJwsEs256DerInvalid => None, Self::KP0010KeyObjectSignerToVerifier => None,
OperationError::KP0014KeyObjectSignerToVerifier => None, Self::KP0011KeyObjectMissingClass => None,
OperationError::KP0015KeyObjectJwsEs256DerInvalid => None, Self::KP0012KeyObjectMissingProvider => None,
OperationError::KP0016KeyObjectJwsEs256DerInvalid => None, Self::KP0012KeyProviderNotLoaded => None,
OperationError::KP0017KeyProviderNoSuchKey => None, Self::KP0013KeyObjectJwsEs256DerInvalid => None,
OperationError::KP0018KeyProviderNoSuchKey => None, Self::KP0014KeyObjectSignerToVerifier => None,
OperationError::KP0019KeyProviderUnsupportedAlgorithm => None, Self::KP0015KeyObjectJwsEs256DerInvalid => None,
OperationError::KP0020KeyObjectNoActiveSigningKeys => None, Self::KP0016KeyObjectJwsEs256DerInvalid => None,
OperationError::KP0021KeyObjectJwsEs256Signature => None, Self::KP0017KeyProviderNoSuchKey => None,
OperationError::KP0022KeyObjectJwsNotAssociated => None, Self::KP0018KeyProviderNoSuchKey => None,
OperationError::KP0023KeyObjectJwsKeyRevoked => None, Self::KP0019KeyProviderUnsupportedAlgorithm => None,
OperationError::KP0024KeyObjectJwsInvalid => None, Self::KP0020KeyObjectNoActiveSigningKeys => None,
OperationError::KP0025KeyProviderNotAvailable => None, Self::KP0021KeyObjectJwsEs256Signature => None,
OperationError::KP0026KeyObjectNoSuchKey => None, Self::KP0022KeyObjectJwsNotAssociated => None,
OperationError::KP0027KeyObjectPublicToDer => None, Self::KP0023KeyObjectJwsKeyRevoked => None,
OperationError::KP0028KeyObjectImportJwsEs256DerInvalid => None, Self::KP0024KeyObjectJwsInvalid => None,
OperationError::KP0029KeyObjectSignerToVerifier => None, Self::KP0025KeyProviderNotAvailable => None,
OperationError::KP0030KeyObjectPublicToDer => None, Self::KP0026KeyObjectNoSuchKey => None,
OperationError::KP0031KeyObjectNotFound => None, Self::KP0027KeyObjectPublicToDer => None,
OperationError::KP0032KeyProviderNoSuchKey => None, Self::KP0028KeyObjectImportJwsEs256DerInvalid => None,
OperationError::KP0033KeyProviderNoSuchKey => None, Self::KP0029KeyObjectSignerToVerifier => None,
OperationError::KP0034KeyProviderUnsupportedAlgorithm => None, Self::KP0030KeyObjectPublicToDer => None,
OperationError::KP0035KeyObjectJweA128GCMGeneration => None, Self::KP0031KeyObjectNotFound => None,
OperationError::KP0036KeyObjectPrivateToBytes => None, Self::KP0032KeyProviderNoSuchKey => None,
OperationError::KP0037KeyObjectImportJweA128GCMInvalid => None, Self::KP0033KeyProviderNoSuchKey => None,
OperationError::KP0038KeyObjectImportJweA128GCMInvalid => None, Self::KP0034KeyProviderUnsupportedAlgorithm => None,
OperationError::KP0039KeyObjectJweNotAssociated => None, Self::KP0035KeyObjectJweA128GCMGeneration => None,
OperationError::KP0040KeyObjectJweInvalid => None, Self::KP0036KeyObjectPrivateToBytes => None,
OperationError::KP0041KeyObjectJweRevoked => None, Self::KP0037KeyObjectImportJweA128GCMInvalid => None,
OperationError::KP0042KeyObjectNoActiveEncryptionKeys => None, Self::KP0038KeyObjectImportJweA128GCMInvalid => None,
OperationError::KP0043KeyObjectJweA128GCMEncryption => None, Self::KP0039KeyObjectJweNotAssociated => None,
OperationError::KP0044KeyObjectJwsPublicJwk => None, Self::KP0040KeyObjectJweInvalid => None,
OperationError::PL0001GidOverlapsSystemRange => None, Self::KP0041KeyObjectJweRevoked => None,
Self::KP0042KeyObjectNoActiveEncryptionKeys => None,
Self::KP0043KeyObjectJweA128GCMEncryption => None,
Self::KP0044KeyObjectJwsPublicJwk => None,
Self::PL0001GidOverlapsSystemRange => None,
} }
} }
} }

View file

@ -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( #[instrument(
level = "info", level = "info",
skip_all, skip_all,

View file

@ -97,6 +97,8 @@ impl Modify for SecurityAddon {
super::v1::person_id_put_attr, super::v1::person_id_put_attr,
super::v1::person_id_post_attr, super::v1::person_id_post_attr,
super::v1::person_id_delete_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_get_id_credential_status,
super::v1::person_id_credential_update_get, super::v1::person_id_credential_update_get,
super::v1::person_id_credential_update_intent_get, super::v1::person_id_credential_update_intent_get,

View file

@ -39,10 +39,11 @@ use hyper::body::Incoming;
use hyper_util::rt::{TokioExecutor, TokioIo}; use hyper_util::rt::{TokioExecutor, TokioIo};
use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID}; use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID};
use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor}; use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor};
use openssl::nid;
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod, SslSessionCacheMode, SslVerifyMode}; use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod, SslSessionCacheMode, SslVerifyMode};
use openssl::x509::X509; use openssl::x509::X509;
use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate};
use sketching::*; use sketching::*;
use tokio::{ use tokio::{
net::{TcpListener, TcpStream}, net::{TcpListener, TcpStream},
@ -554,8 +555,8 @@ pub(crate) async fn handle_conn(
std::io::Error::from(ErrorKind::ConnectionAborted) std::io::Error::from(ErrorKind::ConnectionAborted)
})?; })?;
let mut tls_stream = SslStream::new(ssl, stream).map_err(|e| { let mut tls_stream = SslStream::new(ssl, stream).map_err(|err| {
error!("Failed to create TLS stream: {:?}", e); error!(?err, "Failed to create TLS stream");
std::io::Error::from(ErrorKind::ConnectionAborted) 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() { let client_cert = if let Some(peer_cert) = tls_stream.ssl().peer_certificate() {
// TODO: This is where we should be checking the CRL!!! // TODO: This is where we should be checking the CRL!!!
let subject_key_id = peer_cert // Extract the cert from openssl to x509-cert which is a better
.subject_key_id() // parser to handle the various extensions.
.map(|ski| ski.as_slice().to_vec());
let cn = if let Some(cn) = peer_cert let cert_der = peer_cert.to_der().map_err(|ossl_err| {
.subject_name() error!(?ossl_err, "unable to process x509 certificate as DER");
.entries_by_nid(nid::Nid::COMMONNAME) std::io::Error::from(ErrorKind::ConnectionAborted)
.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
};
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 { } else {
None None
}; };

View file

@ -239,6 +239,8 @@ pub async fn json_rest_event_get(
.map_err(WebError::from) .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( pub async fn json_rest_event_get_id(
state: ServerState, state: ServerState,
id: String, id: String,
@ -258,6 +260,24 @@ pub async fn json_rest_event_get_id(
.map_err(WebError::from) .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( pub async fn json_rest_event_delete_id(
state: ServerState, state: ServerState,
id: String, 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 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 == // // == account ==
#[utoipa::path( #[utoipa::path(
@ -3085,16 +3158,14 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
.post(person_id_post_attr) .post(person_id_post_attr)
.delete(person_id_delete_attr), .delete(person_id_delete_attr),
) )
// .route("/v1/person/:id/_lock", get(|| async { "TODO" })) .route(
// .route("/v1/person/:id/_credential", get(|| async { "TODO" })) "/v1/person/:id/_certificate",
get(person_get_id_certificate).post(person_post_id_certificate),
)
.route( .route(
"/v1/person/:id/_credential/_status", "/v1/person/:id/_credential/_status",
get(person_get_id_credential_status), get(person_get_id_credential_status),
) )
// .route(
// "/v1/person/:id/_credential/:cid/_lock",
// get(|| async { "TODO" }),
// )
.route( .route(
"/v1/person/:id/_credential/_update", "/v1/person/:id/_credential/_update",
get(person_id_credential_update_get), get(person_id_credential_update_get),

View file

@ -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)] #[derive(Serialize, Deserialize, Debug)]
pub enum DbValueV1 { pub enum DbValueV1 {
#[serde(rename = "U8")] #[serde(rename = "U8")]
@ -777,6 +782,8 @@ pub enum DbValueSetV2 {
KeyInternal(Vec<DbValueKeyInternal>), KeyInternal(Vec<DbValueKeyInternal>),
#[serde(rename = "HS")] #[serde(rename = "HS")]
HexString(Vec<String>), HexString(Vec<String>),
#[serde(rename = "X509")]
Certificate(Vec<DbValueCertificate>),
} }
impl DbValueSetV2 { impl DbValueSetV2 {
@ -828,6 +835,7 @@ impl DbValueSetV2 {
DbValueSetV2::CredentialType(set) => set.len(), DbValueSetV2::CredentialType(set) => set.len(),
DbValueSetV2::WebauthnAttestationCaList { ca_list } => ca_list.len(), DbValueSetV2::WebauthnAttestationCaList { ca_list } => ca_list.len(),
DbValueSetV2::KeyInternal(set) => set.len(), DbValueSetV2::KeyInternal(set) => set.len(),
DbValueSetV2::Certificate(set) => set.len(),
} }
} }

View file

@ -2209,3 +2209,38 @@ lazy_static! {
..Default::default() ..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()
};
}

View file

@ -59,6 +59,7 @@ pub enum Attribute {
AuthSessionExpiry, AuthSessionExpiry,
AuthPasswordMinimumLength, AuthPasswordMinimumLength,
BadlistPassword, BadlistPassword,
Certificate,
Claim, Claim,
Class, Class,
ClassName, ClassName,
@ -153,6 +154,7 @@ pub enum Attribute {
PrivilegeExpiry, PrivilegeExpiry,
RadiusSecret, RadiusSecret,
RecycledDirectMemberOf, RecycledDirectMemberOf,
Refers,
Replicated, Replicated,
Rs256PrivateKeyDer, Rs256PrivateKeyDer,
Scope, Scope,
@ -256,6 +258,7 @@ impl TryFrom<String> for Attribute {
ATTR_AUTH_SESSION_EXPIRY => Attribute::AuthSessionExpiry, ATTR_AUTH_SESSION_EXPIRY => Attribute::AuthSessionExpiry,
ATTR_AUTH_PASSWORD_MINIMUM_LENGTH => Attribute::AuthPasswordMinimumLength, ATTR_AUTH_PASSWORD_MINIMUM_LENGTH => Attribute::AuthPasswordMinimumLength,
ATTR_BADLIST_PASSWORD => Attribute::BadlistPassword, ATTR_BADLIST_PASSWORD => Attribute::BadlistPassword,
ATTR_CERTIFICATE => Attribute::Certificate,
ATTR_CLAIM => Attribute::Claim, ATTR_CLAIM => Attribute::Claim,
ATTR_CLASS => Attribute::Class, ATTR_CLASS => Attribute::Class,
ATTR_CLASSNAME => Attribute::ClassName, ATTR_CLASSNAME => Attribute::ClassName,
@ -351,6 +354,7 @@ impl TryFrom<String> for Attribute {
ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry, ATTR_PRIVILEGE_EXPIRY => Attribute::PrivilegeExpiry,
ATTR_RADIUS_SECRET => Attribute::RadiusSecret, ATTR_RADIUS_SECRET => Attribute::RadiusSecret,
ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf, ATTR_RECYCLEDDIRECTMEMBEROF => Attribute::RecycledDirectMemberOf,
ATTR_REFERS => Attribute::Refers,
ATTR_REPLICATED => Attribute::Replicated, ATTR_REPLICATED => Attribute::Replicated,
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer, ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
ATTR_SCOPE => Attribute::Scope, ATTR_SCOPE => Attribute::Scope,
@ -429,6 +433,7 @@ impl From<Attribute> for &'static str {
Attribute::AuthSessionExpiry => ATTR_AUTH_SESSION_EXPIRY, Attribute::AuthSessionExpiry => ATTR_AUTH_SESSION_EXPIRY,
Attribute::AuthPasswordMinimumLength => ATTR_AUTH_PASSWORD_MINIMUM_LENGTH, Attribute::AuthPasswordMinimumLength => ATTR_AUTH_PASSWORD_MINIMUM_LENGTH,
Attribute::BadlistPassword => ATTR_BADLIST_PASSWORD, Attribute::BadlistPassword => ATTR_BADLIST_PASSWORD,
Attribute::Certificate => ATTR_CERTIFICATE,
Attribute::Claim => ATTR_CLAIM, Attribute::Claim => ATTR_CLAIM,
Attribute::Class => ATTR_CLASS, Attribute::Class => ATTR_CLASS,
Attribute::ClassName => ATTR_CLASSNAME, Attribute::ClassName => ATTR_CLASSNAME,
@ -524,6 +529,7 @@ impl From<Attribute> for &'static str {
Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY, Attribute::PrivilegeExpiry => ATTR_PRIVILEGE_EXPIRY,
Attribute::RadiusSecret => ATTR_RADIUS_SECRET, Attribute::RadiusSecret => ATTR_RADIUS_SECRET,
Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF, Attribute::RecycledDirectMemberOf => ATTR_RECYCLEDDIRECTMEMBEROF,
Attribute::Refers => ATTR_REFERS,
Attribute::Replicated => ATTR_REPLICATED, Attribute::Replicated => ATTR_REPLICATED,
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER, Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
Attribute::Scope => ATTR_SCOPE, Attribute::Scope => ATTR_SCOPE,
@ -624,6 +630,7 @@ pub enum EntryClass {
Builtin, Builtin,
Class, Class,
ClassType, ClassType,
ClientCertificate,
Conflict, Conflict,
DomainInfo, DomainInfo,
DynGroup, DynGroup,
@ -677,6 +684,7 @@ impl From<EntryClass> for &'static str {
EntryClass::Builtin => ENTRYCLASS_BUILTIN, EntryClass::Builtin => ENTRYCLASS_BUILTIN,
EntryClass::Class => ATTR_CLASS, EntryClass::Class => ATTR_CLASS,
EntryClass::ClassType => "classtype", EntryClass::ClassType => "classtype",
EntryClass::ClientCertificate => "client_certificate",
EntryClass::Conflict => "conflict", EntryClass::Conflict => "conflict",
EntryClass::DomainInfo => "domain_info", EntryClass::DomainInfo => "domain_info",
EntryClass::DynGroup => ATTR_DYNGROUP, EntryClass::DynGroup => ATTR_DYNGROUP,

View file

@ -248,6 +248,16 @@ lazy_static! {
..Default::default() ..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. /// Builtin IDM Group for granting elevated group write and lifecycle permissions.
pub static ref IDM_GROUP_ADMINS_V1: BuiltinGroup = BuiltinGroup { pub static ref IDM_GROUP_ADMINS_V1: BuiltinGroup = BuiltinGroup {
name: "idm_group_admins", name: "idm_group_admins",
@ -367,6 +377,36 @@ lazy_static! {
], ],
..Default::default() ..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 /// 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

View file

@ -706,6 +706,24 @@ pub static ref SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_DL7: SchemaAttribute = Schem
..Default::default() ..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 === // === classes ===
pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass { 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() ..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()
};
); );

View file

@ -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_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_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_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"); 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_PATCH_LEVEL: Uuid = uuid!("00000000-0000-0000-0000-ffff00000175");
pub const UUID_SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT: Uuid = pub const UUID_SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000176"); 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 // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // 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"); 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_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_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_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 // End of system ranges
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe"); pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");

View file

@ -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( pub fn check_within_valid_time(
ct: Duration, ct: Duration,
valid_from: Option<&OffsetDateTime>, valid_from: Option<&OffsetDateTime>,
@ -416,11 +455,14 @@ impl Account {
vmin && vmax 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 { 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()) 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> { pub fn related_inputs(&self) -> Vec<&str> {
let mut inputs = Vec::with_capacity(4 + self.mail.len()); let mut inputs = Vec::with_capacity(4 + self.mail.len());
self.mail.iter().for_each(|m| { self.mail.iter().for_each(|m| {

View file

@ -24,6 +24,7 @@ pub(crate) mod unix;
use crate::server::identity::Source; use crate::server::identity::Source;
use compact_jwt::JwsCompact; use compact_jwt::JwsCompact;
use kanidm_lib_crypto::{x509_cert::Certificate, Sha256Digest};
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech}; use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
use std::fmt; use std::fmt;
@ -55,8 +56,8 @@ pub struct ClientAuthInfo {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ClientCertInfo { pub struct ClientCertInfo {
pub subject_key_id: Option<Vec<u8>>, pub public_key_s256: Sha256Digest,
pub cn: Option<String>, pub certificate: Certificate,
} }
#[cfg(test)] #[cfg(test)]

View file

@ -367,10 +367,8 @@ pub trait IdmServerTransaction<'a> {
} = client_auth_info; } = client_auth_info;
match (client_cert, bearer_token) { match (client_cert, bearer_token) {
(Some(_client_cert_info), _) => { (Some(client_cert_info), _) => {
// TODO: Cert validation here. self.client_certificate_to_identity(&client_cert_info, ct, source)
warn!("Unable to process client certificate identity");
Err(OperationError::NotAuthenticated)
} }
(None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? { (None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
Token::UserAuthToken(uat) => self.process_uat_to_identity(&uat, ct, source), 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)] #[instrument(level = "info", skip_all)]
fn validate_client_auth_info_to_uat( fn validate_client_auth_info_to_uat(
&mut self, &mut self,
@ -399,10 +400,8 @@ pub trait IdmServerTransaction<'a> {
} = client_auth_info; } = client_auth_info;
match (client_cert, bearer_token) { match (client_cert, bearer_token) {
(Some(_client_cert_info), _) => { (Some(client_cert_info), _) => {
// TODO: Cert validation here. self.client_certificate_to_user_auth_token(&client_cert_info, ct)
warn!("Unable to process client certificate identity");
Err(OperationError::NotAuthenticated)
} }
(None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? { (None, Some(token)) => match self.validate_and_parse_token_to_token(&token, ct)? {
Token::UserAuthToken(uat) => Ok(uat), 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)] #[instrument(level = "debug", skip_all)]
fn validate_ldap_session( fn validate_ldap_session(
&mut self, &mut self,

View file

@ -1,6 +1,7 @@
use super::cid::Cid; use super::cid::Cid;
use super::entry::EntryChangeState; use super::entry::EntryChangeState;
use super::entry::State; use super::entry::State;
use crate::be::dbvalue::DbValueCertificate;
use crate::be::dbvalue::DbValueImage; use crate::be::dbvalue::DbValueImage;
use crate::be::dbvalue::DbValueKeyInternal; use crate::be::dbvalue::DbValueKeyInternal;
use crate::be::dbvalue::DbValueOauthClaimMapJoinV1; use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
@ -459,6 +460,9 @@ pub enum ReplAttrV1 {
HexString { HexString {
set: Vec<String>, set: Vec<String>,
}, },
Certificate {
set: Vec<DbValueCertificate>,
},
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]

View file

@ -237,8 +237,9 @@ impl SchemaAttribute {
SyntaxType::Image => matches!(v, PartialValue::Utf8(_)), SyntaxType::Image => matches!(v, PartialValue::Utf8(_)),
SyntaxType::CredentialType => matches!(v, PartialValue::CredentialType(_)), SyntaxType::CredentialType => matches!(v, PartialValue::CredentialType(_)),
SyntaxType::HexString => matches!(v, PartialValue::HexString(_)), SyntaxType::HexString | SyntaxType::Certificate | SyntaxType::KeyInternal => {
SyntaxType::KeyInternal => matches!(v, PartialValue::HexString(_)), matches!(v, PartialValue::HexString(_))
}
SyntaxType::WebauthnAttestationCaList => false, SyntaxType::WebauthnAttestationCaList => false,
}; };
@ -303,6 +304,7 @@ impl SchemaAttribute {
} }
SyntaxType::KeyInternal => matches!(v, Value::KeyInternal { .. }), SyntaxType::KeyInternal => matches!(v, Value::KeyInternal { .. }),
SyntaxType::HexString => matches!(v, Value::HexString(_)), SyntaxType::HexString => matches!(v, Value::HexString(_)),
SyntaxType::Certificate => matches!(v, Value::Certificate(_)),
}; };
if r { if r {
Ok(()) Ok(())

View file

@ -679,9 +679,12 @@ impl<'a> QueryServerWriteTransaction<'a> {
let idm_schema_classes = [ let idm_schema_classes = [
SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(), SCHEMA_ATTR_PATCH_LEVEL_DL7.clone().into(),
SCHEMA_ATTR_DOMAIN_DEVELOPMENT_TAINT_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_DOMAIN_INFO_DL7.clone().into(),
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(), SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(), SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(),
]; ];
idm_schema_classes idm_schema_classes
@ -700,6 +703,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
.clone() .clone()
.try_into()?, .try_into()?,
IDM_PEOPLE_SELF_MAIL_WRITE_DL7.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 idm_data
@ -715,6 +722,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
let idm_data = [ let idm_data = [
IDM_ACP_SELF_WRITE_DL7.clone().into(), IDM_ACP_SELF_WRITE_DL7.clone().into(),
IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(), IDM_ACP_SELF_NAME_WRITE_DL7.clone().into(),
IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER_DL7.clone().into(),
]; ];
idm_data idm_data

View file

@ -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::KeyInternal => Err(OperationError::InvalidAttribute("Internal keys are generated and not able to be set.".to_string())),
SyntaxType::HexString => Value::new_hex_string_s(value) SyntaxType::HexString => Value::new_hex_string_s(value)
.ok_or_else(|| OperationError::InvalidAttribute("Invalid hex string syntax".to_string())), .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 => { None => {
@ -781,10 +783,10 @@ pub trait QueryServerTransaction<'a> {
SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute( SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
"Invalid - unable to query attestation CA list".to_string(), "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(|| { PartialValue::new_hex_string_s(value).ok_or_else(|| {
OperationError::InvalidAttribute( OperationError::InvalidAttribute(
"Invalid key identifer syntax, expected hex string".to_string(), "Invalid syntax, expected hex string".to_string(),
) )
}) })
} }

View file

@ -17,6 +17,7 @@ use std::time::Duration;
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer}; use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
use hashbrown::HashSet; use hashbrown::HashSet;
use kanidm_lib_crypto::x509_cert::{der::DecodePem, Certificate};
use kanidm_proto::internal::ImageValue; use kanidm_proto::internal::ImageValue;
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use openssl::ec::EcKey; use openssl::ec::EcKey;
@ -275,6 +276,7 @@ pub enum SyntaxType {
OauthClaimMap = 37, OauthClaimMap = 37,
KeyInternal = 38, KeyInternal = 38,
HexString = 39, HexString = 39,
Certificate = 40,
} }
impl TryFrom<&str> for SyntaxType { impl TryFrom<&str> for SyntaxType {
@ -323,6 +325,7 @@ impl TryFrom<&str> for SyntaxType {
"OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap), "OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap),
"KEY_INTERNAL" => Ok(SyntaxType::KeyInternal), "KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
"HEX_STRING" => Ok(SyntaxType::HexString), "HEX_STRING" => Ok(SyntaxType::HexString),
"CERTIFICATE" => Ok(SyntaxType::Certificate),
_ => Err(()), _ => Err(()),
} }
} }
@ -371,6 +374,7 @@ impl fmt::Display for SyntaxType {
SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP", SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP",
SyntaxType::KeyInternal => "KEY_INTERNAL", SyntaxType::KeyInternal => "KEY_INTERNAL",
SyntaxType::HexString => "HEX_STRING", SyntaxType::HexString => "HEX_STRING",
SyntaxType::Certificate => "CERTIFICATE",
}) })
} }
} }
@ -1181,6 +1185,8 @@ pub enum Value {
}, },
HexString(String), HexString(String),
Certificate(Certificate),
} }
impl PartialEq for Value { 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! /// Want a `Value::Image`? use this!
pub fn new_image(input: &str) -> Result<Self, OperationError> { pub fn new_image(input: &str) -> Result<Self, OperationError> {
serde_json::from_str::<ImageValue>(input) serde_json::from_str::<ImageValue>(input)
@ -2008,6 +2018,7 @@ impl Value {
Value::PhoneNumber(_, _) => true, Value::PhoneNumber(_, _) => true,
Value::Address(_) => true, Value::Address(_) => true,
Value::Certificate(_) => true,
Value::Uuid(_) Value::Uuid(_)
| Value::Bool(_) | Value::Bool(_)

View 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)
}
}

View file

@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer}; use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
use dyn_clone::DynClone; use dyn_clone::DynClone;
use hashbrown::HashSet; use hashbrown::HashSet;
use kanidm_lib_crypto::{x509_cert::Certificate, Sha256Digest};
use kanidm_proto::internal::ImageValue; use kanidm_proto::internal::ImageValue;
use openssl::ec::EcKey; use openssl::ec::EcKey;
use openssl::pkey::Private; 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::auditlogstring::{ValueSetAuditLogString, AUDIT_LOG_STRING_CAPACITY};
pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary}; pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary};
pub use self::bool::ValueSetBool; pub use self::bool::ValueSetBool;
pub use self::certificate::ValueSetCertificate;
pub use self::cid::ValueSetCid; pub use self::cid::ValueSetCid;
pub use self::cred::{ pub use self::cred::{
ValueSetAttestedPasskey, ValueSetCredential, ValueSetCredentialType, ValueSetIntentToken, ValueSetAttestedPasskey, ValueSetCredential, ValueSetCredentialType, ValueSetIntentToken,
@ -64,6 +66,7 @@ mod address;
mod auditlogstring; mod auditlogstring;
mod binary; mod binary;
mod bool; mod bool;
mod certificate;
mod cid; mod cid;
mod cred; mod cred;
mod datetime; mod datetime;
@ -612,6 +615,16 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
None 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( fn repl_merge_valueset(
&self, &self,
_older: &ValueSet, _older: &ValueSet,
@ -688,6 +701,7 @@ pub fn from_result_value_iter(
Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k), Value::EcKeyPrivate(k) => ValueSetEcKeyPrivate::new(&k),
Value::Image(imagevalue) => image::ValueSetImage::new(imagevalue), Value::Image(imagevalue) => image::ValueSetImage::new(imagevalue),
Value::CredentialType(c) => ValueSetCredentialType::new(c), Value::CredentialType(c) => ValueSetCredentialType::new(c),
Value::Certificate(c) => ValueSetCertificate::new(c)?,
Value::WebauthnAttestationCaList(_) Value::WebauthnAttestationCaList(_)
| Value::PhoneNumber(_, _) | Value::PhoneNumber(_, _)
| Value::Passkey(_, _, _) | Value::Passkey(_, _, _)
@ -778,6 +792,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
status_cid, status_cid,
der, der,
} => ValueSetKeyInternal::new(id, usage, valid_from, status, status_cid, der), } => ValueSetKeyInternal::new(id, usage, valid_from, status, status_cid, der),
Value::Certificate(certificate) => ValueSetCertificate::new(certificate)?,
Value::PhoneNumber(_, _) => { Value::PhoneNumber(_, _) => {
debug_assert!(false); 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::OauthClaimMap(set) => ValueSetOauthClaimMap::from_dbvs2(set),
DbValueSetV2::KeyInternal(set) => ValueSetKeyInternal::from_dbvs2(set), DbValueSetV2::KeyInternal(set) => ValueSetKeyInternal::from_dbvs2(set),
DbValueSetV2::HexString(set) => ValueSetHexString::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::OauthClaimMap { set } => ValueSetOauthClaimMap::from_repl_v1(set),
ReplAttrV1::KeyInternal { set } => ValueSetKeyInternal::from_repl_v1(set), ReplAttrV1::KeyInternal { set } => ValueSetKeyInternal::from_repl_v1(set),
ReplAttrV1::HexString { set } => ValueSetHexString::from_repl_v1(set), ReplAttrV1::HexString { set } => ValueSetHexString::from_repl_v1(set),
ReplAttrV1::Certificate { set } => ValueSetCertificate::from_repl_v1(set),
} }
} }

View file

@ -51,7 +51,7 @@ shellexpand = { workspace = true }
time = { workspace = true, features = ["serde", "std"] } time = { workspace = true, features = ["serde", "std"] }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } 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"] } url = { workspace = true, features = ["serde"] }
uuid = { workspace = true } uuid = { workspace = true }
zxcvbn = { workspace = true } zxcvbn = { workspace = true }

View file

@ -24,8 +24,8 @@ use uuid::Uuid;
use crate::webauthn::get_authenticator; use crate::webauthn::get_authenticator;
use crate::{ use crate::{
handle_client_error, password_prompt, AccountCredential, AccountRadius, AccountSsh, handle_client_error, password_prompt, AccountCertificate, AccountCredential, AccountRadius,
AccountUserAuthToken, AccountValidity, OutputMode, PersonOpt, PersonPosix, AccountSsh, AccountUserAuthToken, AccountValidity, OutputMode, PersonOpt, PersonPosix,
}; };
impl PersonOpt { impl PersonOpt {
@ -62,6 +62,10 @@ impl PersonOpt {
AccountValidity::ExpireAt(ano) => ano.copt.debug, AccountValidity::ExpireAt(ano) => ano.copt.debug,
AccountValidity::BeginFrom(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 }, // 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");
};
}
} }
} }
} }

View file

@ -531,6 +531,24 @@ pub enum AccountValidity {
BeginFrom(AccountNamedValidDateTimeOpt), 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)] #[derive(Debug, Subcommand)]
pub enum AccountUserAuthToken { pub enum AccountUserAuthToken {
/// Show the status of logged in sessions associated to this account. /// Show the status of logged in sessions associated to this account.
@ -603,6 +621,11 @@ pub enum PersonOpt {
#[clap(subcommand)] #[clap(subcommand)]
commands: AccountValidity, commands: AccountValidity,
}, },
#[clap(name = "certificate", hide = true)]
Certificate {
#[clap(subcommand)]
commands: AccountCertificate,
}
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]