diff --git a/src/core.rs b/src/core.rs index 317a1f2..bead0e6 100644 --- a/src/core.rs +++ b/src/core.rs @@ -26,10 +26,7 @@ use crate::{ module_manager::ModuleManager, modules::{ auth::AuthModule, - credential::{ - userpass::UserPassModule, - approle::AppRoleModule, - }, + credential::{cert::CertModule, approle::AppRoleModule, userpass::UserPassModule}, pki::PkiModule, }, mount::MountTable, @@ -124,6 +121,10 @@ impl Core { let approle_module = AppRoleModule::new(self); self.module_manager.add_module(Arc::new(RwLock::new(Box::new(approle_module))))?; + // add credential module: cert + let cert_module = CertModule::new(self); + self.module_manager.add_module(Arc::new(RwLock::new(Box::new(cert_module))))?; + Ok(()) } diff --git a/src/modules/credential/cert/mod.rs b/src/modules/credential/cert/mod.rs new file mode 100644 index 0000000..e1399b1 --- /dev/null +++ b/src/modules/credential/cert/mod.rs @@ -0,0 +1,907 @@ +//! The `cert` auth method allows authentication using SSL/TLS client certificates which are +//! either signed by a CA or self-signed. SSL/TLS client certificates are defined as having +//! an `ExtKeyUsage` extension with the usage set to either `ClientAuth` or `Any`. +//! +//! The trusted certificates and CAs are configured directly to the auth method using the  +//! `certs/` path. This method cannot read trusted certificates from an external source. +//! +//! CA certificates are associated with a role; role names and CRL names are normalized +//! to lower-case. +//! +//! Please note that to use this auth method, `tls_disable` and `tls_disable_client_certs`  +//! must be false in the RustyVault configuration. This is because the certificates are +//! sent through TLS communication itself. + +use std::sync::{Arc, RwLock}; + +use as_any::Downcast; +use dashmap::DashMap; +use derive_more::Deref; + +use crate::{ + core::Core, + errors::RvError, + logical::{Backend, LogicalBackend, Request, Response}, + modules::{auth::AuthModule, Module}, + new_logical_backend, new_logical_backend_internal, +}; + +pub mod path_certs; +pub mod path_config; +pub mod path_crls; +pub mod path_login; + +pub use path_certs::CertEntry; +pub use path_crls::CRLInfo; + +static CERT_BACKEND_HELP: &str = r#" +The "cert" credential provider allows authentication using +TLS client certificates. A client connects to RustyVault and uses +the "login" endpoint to generate a client token. + +Trusted certificates are configured using the "certs/" endpoint +by a user with root access. A certificate authority can be trusted, +which permits all keys signed by it. Alternatively, self-signed +certificates can be trusted avoiding the need for a CA. +"#; + +pub struct CertModule { + pub name: String, + pub backend: Arc, +} + +pub struct CertBackendInner { + pub core: Arc>, + pub crls: DashMap, +} + +#[derive(Deref)] +pub struct CertBackend { + #[deref] + pub inner: Arc, +} + +impl CertBackend { + pub fn new(core: Arc>) -> Self { + let inner = CertBackendInner { core, crls: DashMap::new() }; + Self { inner: Arc::new(inner) } + } + + pub fn new_backend(&self) -> LogicalBackend { + let cert_backend_ref = Arc::clone(&self.inner); + + let mut backend = new_logical_backend!({ + unauth_paths: ["login"], + auth_renew_handler: cert_backend_ref.renew_path_login, + help: CERT_BACKEND_HELP, + }); + + backend.paths.push(Arc::new(self.config_path())); + backend.paths.push(Arc::new(self.certs_path())); + backend.paths.push(Arc::new(self.certs_list_path())); + backend.paths.push(Arc::new(self.crl_path())); + backend.paths.push(Arc::new(self.crl_list_path())); + backend.paths.push(Arc::new(self.login_path())); + + backend + } +} + +impl CertBackendInner { + pub fn renew_path_login(&self, _backend: &dyn Backend, _req: &mut Request) -> Result, RvError> { + Ok(None) + } +} + +impl CertModule { + pub fn new(core: &Core) -> Self { + Self { + name: "cert".to_string(), + backend: Arc::new(CertBackend::new(Arc::clone(core.self_ref.as_ref().unwrap()))), + } + } +} + +impl Module for CertModule { + fn name(&self) -> String { + return self.name.clone(); + } + + fn setup(&mut self, core: &Core) -> Result<(), RvError> { + let cert = Arc::clone(&self.backend); + let cert_backend_new_func = move |_c: Arc>| -> Result, RvError> { + let mut cert_backend = cert.new_backend(); + cert_backend.init()?; + Ok(Arc::new(cert_backend)) + }; + + if let Some(module) = core.module_manager.get_module("auth") { + let auth_mod = module.read()?; + if let Some(auth_module) = auth_mod.as_ref().downcast_ref::() { + return auth_module.add_auth_backend("cert", Arc::new(cert_backend_new_func)); + } else { + log::error!("downcast auth module failed!"); + } + } else { + log::error!("get auth module failed!"); + } + + Ok(()) + } + + fn cleanup(&mut self, core: &Core) -> Result<(), RvError> { + if let Some(module) = core.module_manager.get_module("auth") { + let auth_mod = module.read()?; + if let Some(auth_module) = auth_mod.as_ref().downcast_ref::() { + return auth_module.delete_auth_backend("cert"); + } else { + log::error!("downcast auth module failed!"); + } + } else { + log::error!("get auth module failed!"); + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use serde_json::{json, Value, Map}; + use std::time::Duration; + + use super::*; + use crate::{ + test_utils::{ + TestHttpServer, TestTlsClientAuth, new_test_cert, new_test_cert_ext, new_test_crl, + }, + modules::auth::expiration::MAX_LEASE_DURATION_SECS, + }; + + #[derive(Default)] + struct Allowed { + // allowed names in the certificate, looks at common, name, dns, email [depricated] + names: String, + // allowed common names in the certificate + common_names: String, + // allowed dns names in the SAN extension of the certificate + dns: String, + // allowed email names in SAN extension of the certificate + emails: String, + // allowed uris in SAN extension of the certificate + uris: String, + // allowed OUs in the certificate + organizational_units: String, + // required extensions in the certificate + ext: String, + // allowed metadata extensions to add to identity alias + metadata_ext: String, + } + + impl TestHttpServer { + fn test_write_cert(&self, name: &str, cert: &str, policies: &str, test_data: &Allowed, extra_data: &Map, expect_err: Option<&str>) { + let mut data = json!({ + "display_name": name, + "policies": policies, + "certificate": cert, + "lease": 1000, + "allowed_names": &test_data.names, + "allowed_common_names": &test_data.common_names, + "allowed_dns_sans": &test_data.dns, + "allowed_email_sans": &test_data.emails, + "allowed_uri_sans": &test_data.uris, + "allowed_organizational_units": &test_data.organizational_units, + "required_extensions": &test_data.ext, + "allowed_metadata_extensions": &test_data.metadata_ext, + }) + .as_object() + .unwrap() + .clone(); + + for (key, value) in extra_data.iter() { + data.insert(key.clone(), value.clone()); + } + + let ret = self.write(&format!("auth/{}/certs/{}", &self.mount_path, name), Some(data), None); + assert!(ret.is_ok()); + let (status, resp) = ret.unwrap(); + match expect_err { + Some(err) => { + assert_eq!(status, 400); + assert_eq!(resp["error"], err); + }, + _ => { + assert!(status == 200 || status == 204); + } + } + } + + fn test_write_cert_lease(&self, name: &str, cert: &str, policies: &str) { + let data = json!({ + "display_name": name, + "policies": policies, + "certificate": cert, + "lease": 900, + }) + .as_object() + .unwrap() + .clone(); + + let ret = self.write(&format!("auth/{}/certs/{}", &self.mount_path, name), Some(data), None); + assert!(ret.is_ok()); + let (status, _) = ret.unwrap(); + assert!(status == 200 || status == 204); + } + + fn test_write_cert_no_lease(&self, name: &str, cert: &str, policies: &str) { + let data = json!({ + "display_name": name, + "policies": policies, + "certificate": cert, + }) + .as_object() + .unwrap() + .clone(); + + let ret = self.write(&format!("auth/{}/certs/{}", &self.mount_path, name), Some(data), None); + assert!(ret.is_ok()); + let (status, _) = ret.unwrap(); + assert!(status == 200 || status == 204); + } + + fn test_write_cert_ttl(&self, name: &str, cert: &str, policies: &str) { + let data = json!({ + "display_name": name, + "policies": policies, + "certificate": cert, + "ttl": "900s", + }) + .as_object() + .unwrap() + .clone(); + + let ret = self.write(&format!("auth/{}/certs/{}", &self.mount_path, name), Some(data), None); + assert!(ret.is_ok()); + let (status, _) = ret.unwrap(); + assert!(status == 200 || status == 204); + } + + fn test_write_cert_max_ttl(&self, name: &str, cert: &str, policies: &str) { + let data = json!({ + "display_name": name, + "policies": policies, + "certificate": cert, + "ttl": "900s", + "max_ttl": "1200s", + }) + .as_object() + .unwrap() + .clone(); + + let ret = self.write(&format!("auth/{}/certs/{}", &self.mount_path, name), Some(data), None); + assert!(ret.is_ok()); + let (status, _) = ret.unwrap(); + assert!(status == 200 || status == 204); + } + + fn test_read_cert(&self, name: &str) -> Result<(u16, Value), RvError> { + self.read(&format!("auth/{}/certs/{}", &self.mount_path, name), None) + } + + fn test_login(&self, server_ca: &str, client_cert: &str, client_key: &str, expect_err: Option<&str>) -> Result<(u16, Value), RvError> { + self.test_login_with_name("", server_ca, client_cert, client_key, expect_err) + } + + fn test_login_with_name(&self, name: &str, server_ca: &str, client_cert: &str, client_key: &str, expect_err: Option<&str>) -> Result<(u16, Value), RvError> { + let tls_client_auth = TestTlsClientAuth { + ca_pem: server_ca.into(), + cert_pem: client_cert.into(), + key_pem: client_key.into(), + }; + + let data = json!({ + "name": name, + }) + .as_object() + .unwrap() + .clone(); + let ret = self.login(&format!("auth/{}/login", &self.mount_path), Some(data), Some(tls_client_auth)); + assert!(ret.is_ok()); + let (status, resp) = ret.unwrap(); + match expect_err { + Some(err) => { + assert!(status == 400 || status == 403); + assert_eq!(resp["error"], err); + assert!(resp["auth"].is_null()); + }, + _ => { + assert!(resp["auth"].is_object()); + assert_ne!(resp["auth"]["client_token"], ""); + } + } + + Ok((status, resp)) + } + + fn test_login_with_metadata(&self, name: &str, server_ca: &str, client_cert: &str, client_key: &str, meta_data: &Map, expect_err: Option<&str>) -> Result<(u16, Value), RvError> { + let tls_client_auth = TestTlsClientAuth { + ca_pem: server_ca.into(), + cert_pem: client_cert.into(), + key_pem: client_key.into(), + }; + + let data = json!({ + "metadata": meta_data, + }) + .as_object() + .unwrap() + .clone(); + let ret = self.login(&format!("auth/{}/login", &self.mount_path), Some(data), Some(tls_client_auth)); + assert!(ret.is_ok()); + let (status, resp) = ret.unwrap(); + match expect_err { + Some(err) => { + assert_eq!(status, 400); + assert_eq!(resp["error"], err); + assert!(resp["auth"].is_null()); + }, + _ => { + assert!(resp["auth"].is_object()); + assert_ne!(resp["auth"]["client_token"], ""); + for (key, expected) in meta_data.iter() { + assert_eq!(resp["auth"]["metadata"][key], expected.clone()); + } + assert_eq!(resp["auth"]["metadata"]["cert_name"], name); + } + } + + Ok((status, resp)) + } + + fn test_write_crl(&self, revoked_cert: &str, ca_cert: &str, ca_key: &str) { + let crl_pem_ret = unsafe { new_test_crl(revoked_cert, ca_cert, ca_key) }; + assert!(crl_pem_ret.is_ok()); + let crl_pem = crl_pem_ret.unwrap(); + let crl_data = json!({ + "crl": crl_pem, + }) + .as_object() + .unwrap() + .clone(); + let ret = self.write(&format!("auth/{}/crls/test", &self.mount_path), Some(crl_data), None); + assert!(ret.is_ok()); + let (status, _) = ret.unwrap(); + assert!(status == 200 || status == 204); + + // Ensure the CRL shows up on a list. + let ret = self.list("auth/cert/crls", None); + assert!(ret.is_ok()); + + let (status, resp_data) = ret.unwrap(); + assert_eq!(status, 200); + assert_eq!(resp_data["data"]["keys"].as_array().expect("crl list is empty").len(), 1); + } + + fn test_delete_crl(&self) { + let ret = self.delete(&format!("auth/{}/crls/test", &self.mount_path), None, None); + assert!(ret.is_ok()); + let (status, _) = ret.unwrap(); + assert!(status == 200 || status == 204); + + // Ensure the CRL shows up on a list. + let ret = self.list("auth/cert/crls", None); + assert!(ret.is_ok()); + + let (status, resp_data) = ret.unwrap(); + assert_eq!(status, 200); + assert_eq!(resp_data["data"]["keys"].as_array().expect("crl list is empty").len(), 0); + } + } + + #[test] + fn test_credential_cert_module_permitted_dns_domains_intermediate_ca() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_permitted_dns_domains_intermediate_ca", true); + + let (intermediate_ca_cert, intermediate_ca_key) = new_test_cert(true, true, true, "inter", Some(".myrv.com"), None, None, None, Some(test_http_server.ca_cert_pem.clone()), Some(test_http_server.ca_key_pem.clone())).unwrap(); + + let (leaf_cert, leaf_key) = new_test_cert(false, true, true, "cert.myrv.com", Some("cert.myrv.com"), None, None, Some("10s"), Some(intermediate_ca_cert.clone()), Some(intermediate_ca_key)).unwrap(); + + // mount cert auth to path: auth/cert + let _ = test_http_server.mount_auth("cert", "cert"); + + test_http_server.test_write_cert("myrv-dot-com", &intermediate_ca_cert, "default", &Allowed::default(), &Map::new(), None); + + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &leaf_cert, &leaf_key, None); + + // TODO: testing pathLoginRenew for cert auth + } + + #[test] + fn test_credential_cert_module_non_ca_expiry() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_non_ca_expiry", true); + + // mount /pki as a root CA + let ret = test_http_server.mount("pki", "pki"); + assert!(ret.is_ok()); + + let (ca_cert, ca_key) = new_test_cert(true, true, true, "test-ca", None, None, None, None, None, None).unwrap(); + + let (issued_cert, issued_key) = new_test_cert(false, true, true, "cert.myrv.com", Some("cert.myrv.com"), None, None, Some("5s"), Some(ca_cert.clone()), Some(ca_key)).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("myrv-dot-com", &issued_cert, "default", &Allowed::default(), &Map::new(), None); + + // Login when the certificate is still valid. Login should succeed. + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &issued_cert, &issued_key, None); + + // Wait until the certificate expires + std::thread::sleep(Duration::from_secs(6)); + + // Login attempt after certificate expiry should fail + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &issued_cert, &issued_key, Some("certificate has expired")); + } + + #[test] + fn test_credential_cert_module_registered_non_ca_crl() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_registered_non_ca_crl", true); + + let (ca_cert, ca_key) = new_test_cert(true, true, true, "test-ca", None, None, None, None, None, None).unwrap(); + + let (issued_cert, issued_key) = new_test_cert(false, true, true, "cert.myrv.com", Some("cert.myrv.com"), None, None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("myrv-dot-com", &issued_cert, "default", &Allowed::default(), &Map::new(), None); + + // Login when the certificate is still valid. Login should succeed. + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &issued_cert, &issued_key, None); + + // Register a CRL containing the issued client certificate used above. + let _ = test_http_server.test_write_crl(&issued_cert, &ca_cert, &ca_key); + + // Login attempt after certificate expiry should fail + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &issued_cert, &issued_key, Some("invalid certificate or no client certificate supplied")); + } + + #[test] + fn test_credential_cert_module_crls() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_crls", true); + + let (ca_cert, ca_key) = new_test_cert(true, true, true, "test-ca", None, None, None, None, None, None).unwrap(); + + let (issued_cert, issued_key) = new_test_cert(false, true, true, "cert.myrv.com", Some("cert.myrv.com"), None, None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + // Register the CA certificate of the client key pair + test_http_server.test_write_cert("cert1", &ca_cert, "abc", &Allowed::default(), &Map::new(), None); + + // Login with the CA certificate should be successful. + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None); + + // Login with a client certificate issued by this CA certificate should also be successful. + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &issued_cert, &issued_key, None); + + // Register a CRL containing the issued client certificate used above. + test_http_server.test_write_crl(&issued_cert, &ca_cert, &ca_key); + + // Attempt login with the revoked certificate should fail. + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &issued_cert, &issued_key, Some("no chain matching all constraints could be found for this login certificate")); + + // Register a different client CA certificate. + let (ca_cert, ca_key) = new_test_cert(true, true, true, "test-ca2", None, None, None, None, None, None).unwrap(); + test_http_server.test_write_cert("cert1", &ca_cert, "abc", &Allowed::default(), &Map::new(), None); + + // Test login using a different client CA cert pair. + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None); + + // Register a CRL containing the root CA certificate used above. + test_http_server.test_write_crl(&ca_cert, &ca_cert, &ca_key); + + // Attempt login with the revoked ca certificate should fail. + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")); + } + + #[test] + fn test_credential_cert_module_cert_writes() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_cert_writes", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "test-ca", None, None, None, None, None, None).unwrap(); + + // Non CA cert + let (non_ca_cert, _) = new_test_cert(false, true, true, "non-ca-cert", Some("non-ca-cert"), None, None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // Non CA cert without TLS web client authentication + let (non_ca_cert2, _) = new_test_cert(false, false, true, "non-ca-cert2", Some("non-ca-cert2"), None, None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("cert1", &ca_cert, "abc", &Allowed::default(), &Map::new(), None); + test_http_server.test_write_cert("cert1", &non_ca_cert, "abc", &Allowed::default(), &Map::new(), None); + test_http_server.test_write_cert("cert1", &non_ca_cert2, "abc", &Allowed::default(), &Map::new(), Some("nonCA certificates should have TLS client authentication set as an extended key usage")); + } + + #[test] + fn test_credential_cert_module_basic_ca() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_basic_ca", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "test-ca", None, None, None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert(false, true, true, "cert.example.com", Some("cert.example.com"), None, None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed::default(), &Map::new(), None); + + // Test a client trusted by a CA + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None); + let (_, resp) = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + assert_eq!(resp["auth"]["lease_duration"], 1000); + assert_eq!(resp["auth"]["policies"], json!(["default", "foo"])); + + test_http_server.test_write_cert_lease("web", &ca_cert, "foo"); + test_http_server.test_write_cert_ttl("web", &ca_cert, "foo"); + let (_, resp) = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + assert_eq!(resp["auth"]["lease_duration"], 900); + assert_eq!(resp["auth"]["policies"], json!(["default", "foo"])); + + test_http_server.test_write_cert_max_ttl("web", &ca_cert, "foo"); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None); + test_http_server.test_write_cert_no_lease("web", &ca_cert, "foo"); + let (_, resp) = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + assert_eq!(resp["auth"]["lease_duration"], 900); + assert_eq!(resp["auth"]["policies"], json!(["default", "foo"])); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "*.example.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "*.invalid.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + } + + #[test] + fn test_credential_cert_module_basic_crls() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_basic_crls", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "test-ca", None, None, None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert(false, true, true, "cert.example.com", Some("cert.example.com"), None, None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert_no_lease("web", &ca_cert, "foo"); + + let (_, resp) = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + assert_eq!(resp["auth"]["lease_duration"], MAX_LEASE_DURATION_SECS.as_secs()); + assert_eq!(resp["auth"]["policies"], json!(["default", "foo"])); + + test_http_server.test_write_crl(&ca_cert, &ca_cert, &ca_key); + + let (status, resp) = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + assert_eq!(status, 400); + assert!(resp["auth"].is_null()); + + test_http_server.test_delete_crl(); + + let (_, resp) = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + assert_eq!(resp["auth"]["lease_duration"], MAX_LEASE_DURATION_SECS.as_secs()); + assert_eq!(resp["auth"]["policies"], json!(["default", "foo"])); + } + + #[test] + fn test_credential_cert_module_basic_single_cert() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_basic_single_cert", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "example.com", Some("example.com"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed::default(), &Map::new(), None); + + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "invalid".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { common_names: "example.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { common_names: "invalid".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { ext: "1.2.3.4:invalid".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + } + + #[test] + fn test_credential_cert_module_ext_single_cert() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_ext_single_cert", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert_ext(true, true, true, "example.com", Some("example.com"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { ext: "2.1.1.1:A UTF8String Extension".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { ext: "2.1.1.1:*,2.1.1.2:A UTF8*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { ext: "1.2.3.45:*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { ext: "2.1.1.1:The Wrong Value".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { ext: "2.1.1.1:*,2.1.1.2:The Wrong Value".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { ext: "2.1.1.1:".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { ext: "2.1.1.1:,2.1.1.2:*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ext: "2.1.1.1:A UTF8String Extension".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ext: "2.1.1.1:*,2.1.1.2:A UTF8*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ext: "1.2.3.45:*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ext: "2.1.1.1:The Wrong Value".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ext: "2.1.1.1:*,2.1.1.2:The Wrong Value".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "invalid".into(), ext: "2.1.1.1:A UTF8String Extension".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "invalid".into(), ext: "2.1.1.1:*,2.1.1.2:A UTF8*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "invalid".into(), ext: "1.2.3.45:*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "invalid".into(), ext: "2.1.1.1:The Wrong Value".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "invalid".into(), ext: "2.1.1.1:*,2.1.1.2:The Wrong Value".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ext: "hex:2.5.29.17:*87047F000002*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ext: "hex:2.5.29.17:*87047F000001*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { names: "example.com".into(), ext: "2.5.29.17:*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { metadata_ext: "2.1.1.1,1.2.3.45".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login_with_metadata("web", &test_http_server.ca_cert_pem, &ca_cert, &ca_key, json!({"2-1-1-1":"A UTF8String Extension"}).as_object().unwrap(), None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { metadata_ext: "1.2.3.45".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login_with_metadata("web", &test_http_server.ca_cert_pem, &ca_cert, &ca_key, &Map::new(), None).unwrap(); + } + + #[test] + fn test_credential_cert_module_dns_single_cert() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_dns_single_cert", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert(false, true, true, "example.com", Some("example.com"), Some("127.0.0.1"), None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { dns: "example.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { dns: "*ample.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { dns: "notincert.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { dns: "abc".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { dns: "*.example.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + } + + #[test] + fn test_credential_cert_module_email_single_cert() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_email_single_cert", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert_ext(false, true, true, "example.com", Some("example.com"), Some("127.0.0.1"), None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { emails: "valid@example.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { emails: "*@example.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { emails: "invalid@notincert.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { emails: "abc".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { emails: "*.example.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + } + + #[test] + fn test_credential_cert_module_uri_single_cert() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_uri_single_cert", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert(false, true, true, "example.com", Some("example.com"), Some("127.0.0.1"), Some("spiffe://example.com/host"), None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + println!("mount ret: {:?}", ret); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { uris: "spiffe://example.com/*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { uris: "spiffe://example.com/host".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { uris: "spiffe://example.com/invalid".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { uris: "abc".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { uris: "http://www.google.com".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + } + + #[test] + fn test_credential_cert_module_ou_single_cert() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_ou_single_cert", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert_ext(true, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { organizational_units: "engineering".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { organizational_units: "eng*".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { organizational_units: "engineering,finance".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, None).unwrap(); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed { organizational_units: "foo".into(), ..Default::default()}, &Map::new(), None); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &ca_cert, &ca_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + } + + #[test] + fn test_credential_cert_module_mixed_constraints() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_mixed_constraints", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert_ext(true, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert(false, true, true, "cert.example.com", Some("cert.example.com"), Some("127.0.0.1"), None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("1unconstrained", &ca_cert, "foo", &Allowed::default(), &Map::new(), None); + test_http_server.test_write_cert("2matching", &ca_cert, "foo", &Allowed { names: "*.example.com,whatever".into(), ..Default::default()}, &Map::new(), None); + test_http_server.test_write_cert("3invalid", &ca_cert, "foo", &Allowed { names: "invalid".into(), ..Default::default()}, &Map::new(), None); + + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + let _ = test_http_server.test_login_with_name("2matching", &test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + let _ = test_http_server.test_login_with_name("3invalid", &test_http_server.ca_cert_pem, &client_cert, &client_key, Some("no chain matching all constraints could be found for this login certificate")).unwrap(); + } + + #[test] + fn test_credential_cert_module_untrusted() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_untrusted", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert_ext(true, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert(false, true, true, "cert.example.com", Some("cert.example.com"), Some("127.0.0.1"), None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("invalid certificate or no client certificate supplied")).unwrap(); + } + + #[test] + fn test_credential_cert_module_valid_cidr() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_valid_cidr", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert(false, true, true, "cert.example.com", Some("cert.example.com"), Some("127.0.0.1"), None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed::default(), + json!({"bound_cidrs": ["127.0.0.1", "128.252.0.0/16"]}).as_object().unwrap(), None); + let (_, resp) = test_http_server.test_read_cert("web").unwrap(); + assert_eq!(resp["data"]["bound_cidrs"], json!(["127.0.0.1", "128.252.0.0/16"])); + assert_eq!(resp["data"]["token_bound_cidrs"], json!(["127.0.0.1", "128.252.0.0/16"])); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, None).unwrap(); + } + + #[test] + fn test_credential_cert_module_invalid_cidr() { + let mut test_http_server = TestHttpServer::new("test_credential_cert_module_invalid_cidr", true); + + // CA cert + let (ca_cert, ca_key) = new_test_cert(true, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), None, None, None, None).unwrap(); + + let (client_cert, client_key) = new_test_cert(false, true, true, "cert.example.com", Some("cert.example.com"), Some("127.0.0.1"), None, None, Some(ca_cert.clone()), Some(ca_key.clone())).unwrap(); + + // mount cert auth to path: auth/cert + let ret = test_http_server.mount_auth("cert", "cert"); + assert!(ret.is_ok()); + + test_http_server.test_write_cert("web", &ca_cert, "foo", &Allowed::default(), + json!({"bound_cidrs": ["127.0.0.2", "128.252.0.0/16"]}).as_object().unwrap(), None); + let (_, resp) = test_http_server.test_read_cert("web").unwrap(); + assert_eq!(resp["data"]["bound_cidrs"], json!(["127.0.0.2", "128.252.0.0/16"])); + assert_eq!(resp["data"]["token_bound_cidrs"], json!(["127.0.0.2", "128.252.0.0/16"])); + let _ = test_http_server.test_login(&test_http_server.ca_cert_pem, &client_cert, &client_key, Some("Permission denied.")).unwrap(); + } +} diff --git a/src/modules/credential/cert/path_certs.rs b/src/modules/credential/cert/path_certs.rs new file mode 100644 index 0000000..ed02ed7 --- /dev/null +++ b/src/modules/credential/cert/path_certs.rs @@ -0,0 +1,447 @@ +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use derive_more::{Deref, DerefMut}; +use openssl::x509::X509; +use openssl_sys::XKU_SSL_CLIENT; +use serde::{Deserialize, Serialize}; + +use super::{CertBackend, CertBackendInner}; +use crate::{ + context::Context, + errors::RvError, + logical::{field::FieldTrait, Backend, Field, FieldType, Operation, Path, PathOperation, Request, Response}, + new_fields, new_fields_internal, new_path, new_path_internal, rv_error_response, + storage::StorageEntry, + utils::{ + cert::{ + deserialize_vec_x509, has_x509_ext_key_usage, has_x509_ext_key_usage_flag, is_ca_cert, serialize_vec_x509, + }, + deserialize_duration, serialize_duration, + sock_addr::SockAddrMarshaler, + token_util::{token_fields, TokenParams}, + }, +}; + +//const DEFAULT_MAX_TTL: Duration = Duration::from_secs(365*24*60*60 as u64); + +#[derive(Debug, Clone, Default, Serialize, Deserialize, Deref, DerefMut)] +pub struct CertEntry { + pub name: String, + pub display_name: String, + #[serde(serialize_with = "serialize_vec_x509", deserialize_with = "deserialize_vec_x509")] + pub certificate: Vec, + pub policies: Vec, + #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] + pub ttl: Duration, + #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] + pub max_ttl: Duration, + #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] + pub period: Duration, + pub bound_cidrs: Vec, + pub allowed_names: Vec, + pub allowed_common_names: Vec, + pub allowed_dns_sans: Vec, + pub allowed_email_sans: Vec, + pub allowed_uri_sans: Vec, + pub allowed_organizational_units: Vec, + pub allowed_metadata_extensions: Vec, + pub required_extensions: Vec, + pub ocsp_enabled: bool, + #[serde(serialize_with = "serialize_vec_x509", deserialize_with = "deserialize_vec_x509")] + pub ocsp_ca_certificates: Vec, + pub ocsp_servers_override: Vec, + pub ocsp_fail_open: bool, + pub ocsp_query_all_servers: bool, + #[serde(flatten)] + #[deref] + #[deref_mut] + pub token_params: TokenParams, +} + +impl CertBackend { + pub fn certs_path(&self) -> Path { + let cert_backend_ref1 = Arc::clone(&self.inner); + let cert_backend_ref2 = Arc::clone(&self.inner); + let cert_backend_ref3 = Arc::clone(&self.inner); + + let mut path = new_path!({ + pattern: r"certs/(?P\w[\w-]+\w)", + fields: { + "name": { + field_type: FieldType::Str, + required: true, + description: "The name of the certificate." + }, + "certificate": { + field_type: FieldType::Str, + required: true, + description: "The public certificate that should be trusted. Must be x509 PEM encoded." + }, + "ocsp_enabled": { + field_type: FieldType::Bool, + default: false, + description: "Whether to attempt OCSP verification of certificates at login" + }, + "ocsp_ca_certificates": { + field_type: FieldType::Str, + required: false, + description: "Any additional CA certificates needed to communicate with OCSP servers" + }, + "ocsp_servers_override": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated list of OCSP server addresses. +If unset, the OCSP server is determined from the AuthorityInformationAccess extension on +the certificate being inspected."# + }, + "ocsp_fail_open": { + field_type: FieldType::Bool, + default: false, + description: r#"If set to true, if an OCSP revocation cannot +be made successfully, login will proceed rather than failing. If false, failing +to get an OCSP status fails the request."# + }, + "ocsp_query_all_servers": { + field_type: FieldType::Bool, + default: false, + description: r#"If set to true, rather than accepting the first +successful OCSP response, query all servers and consider the certificate valid +only if all servers agree."# + }, + "allowed_names": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated list of names. +At least one must exist in either the Common Name or SANs. Supports globbing. +This parameter is deprecated, please use allowed_common_names, allowed_dns_sans, +allowed_email_sans, allowed_uri_sans."# + }, + "allowed_common_names": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated list of names. + At least one must exist in the Common Name. Supports globbing."# + }, + "allowed_dns_sans": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated list of DNS names. + At least one must exist in the SANs. Supports globbing."# + }, + "allowed_email_sans": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated list of Email Addresses. + At least one must exist in the SANs. Supports globbing."# + }, + "allowed_uri_sans": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated list of URIs. + At least one must exist in the SANs. Supports globbing."# + }, + "allowed_organizational_units": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated list of Organizational Units names. + At least one must exist in the OU field."# + }, + "required_extensions": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated string or array of extensions +formatted as "oid:value". Expects the extension value to be some type of ASN1 encoded string. +All values much match. Supports globbing on "value"."# + }, + "allowed_metadata_extensions": { + field_type: FieldType::CommaStringSlice, + required: false, + description: r#"A comma-separated string or array of oid extensions. +Upon successful authentication, these extensions will be added as metadata if they are present +in the certificate. The metadata key will be the string consisting of the oid numbers +separated by a dash (-) instead of a dot (.) to allow usage in ACL templates."# + }, + "policies": { + field_type: FieldType::CommaStringSlice, + required: false, + description: "Use token_policies instead. If this and token_policies are both speicified, only token_policies will be used." + }, + "lease": { + field_type: FieldType::Int, + required: false, + description: "Use token_ttl instead. If this and token_ttl are both speicified, only token_ttl will be used." + }, + "ttl": { + field_type: FieldType::DurationSecond, + required: false, + description: "Use token_ttl instead. If this and token_ttl are both speicified, only token_ttl will be used." + }, + "max_ttl": { + field_type: FieldType::DurationSecond, + required: false, + description: "Use token_max_ttl instead. If this and token_max_ttl are both speicified, only token_max_ttl will be used." + }, + "period": { + field_type: FieldType::DurationSecond, + default: 0, + description: "Use token_period instead. If this and token_period are both speicified, only token_period will be used." + }, + "bound_cidrs": { + field_type: FieldType::CommaStringSlice, + required: false, + description: "Use token_bound_cidrs instead. If this and token_bound_cidrs are both speicified, only token_bound_cidrs will be used." + }, + "display_name": { + field_type: FieldType::Str, + required: false, + description: "The display name to use for clients using this certificate." + } + }, + operations: [ + {op: Operation::Read, handler: cert_backend_ref1.read_cert}, + {op: Operation::Write, handler: cert_backend_ref2.write_cert}, + {op: Operation::Delete, handler: cert_backend_ref3.delete_cert} + ], + help: r#" +This endpoint allows you to create, read, update, and delete trusted certificates +that are allowed to authenticate. + +Deleting a certificate will not revoke auth for prior authenticated connections. +To do this, do a revoke on "login". If you don't need to revoke login immediately, +then the next renew will cause the lease to expire. + "# + }); + + path.fields.extend(token_fields()); + + path + } + + pub fn certs_list_path(&self) -> Path { + let cert_backend_ref = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"certs/?", + operations: [ + {op: Operation::List, handler: cert_backend_ref.list_cert} + ], + help: r#"This endpoint allows you to list certs"# + }); + + path + } +} + +impl CertBackendInner { + pub fn get_cert(&self, req: &Request, name: &str) -> Result, RvError> { + let key = format!("cert/{}", name.to_lowercase()); + let storage_entry = req.storage_get(&key)?; + if storage_entry.is_none() { + return Ok(None); + } + + let entry = storage_entry.unwrap(); + let cert_entry: CertEntry = serde_json::from_slice(entry.value.as_slice())?; + Ok(Some(cert_entry)) + } + + pub fn set_cert(&self, req: &Request, name: &str, cert_entry: &CertEntry) -> Result<(), RvError> { + let entry = StorageEntry::new(format!("cert/{}", name).as_str(), cert_entry)?; + + req.storage_put(&entry) + } + + pub fn read_cert(&self, _backend: &dyn Backend, req: &Request) -> Result, RvError> { + let name = req.get_data_as_str("name")?.to_lowercase(); + + let entry = self.get_cert(req, &name)?; + if entry.is_none() { + return Ok(None); + } + + let cert_entry = entry.unwrap(); + let mut cert_entry_data = serde_json::to_value(&cert_entry)?; + let data = cert_entry_data.as_object_mut().unwrap(); + + if cert_entry.policies.len() > 0 { + data["policies"] = data["token_policies"].clone(); + } + + if cert_entry.bound_cidrs.len() > 0 { + data["bound_cidrs"] = data["token_bound_cidrs"].clone(); + } + + Ok(Some(Response::data_response(Some(data.clone())))) + } + + pub fn write_cert(&self, _backend: &dyn Backend, req: &Request) -> Result, RvError> { + let name = req.get_data_as_str("name")?.to_lowercase(); + + let mut cert_entry = CertEntry::default(); + + let entry = self.get_cert(req, &name)?; + if entry.is_some() { + cert_entry = entry.unwrap(); + } else { + cert_entry.name = name.clone(); + } + + if let Ok(certificate_raw) = req.get_data("certificate") { + let certificate = certificate_raw.as_str().ok_or(RvError::ErrRequestFieldInvalid)?; + cert_entry.certificate = X509::stack_from_pem(certificate.as_bytes())?; + } + + if let Ok(ocsp_ca_certificates_raw) = req.get_data("ocsp_ca_certificates") { + let ocsp_ca_certificates = ocsp_ca_certificates_raw.as_str().ok_or(RvError::ErrRequestFieldInvalid)?; + cert_entry.ocsp_ca_certificates = X509::stack_from_pem(ocsp_ca_certificates.as_bytes())?; + } + + if let Ok(ocsp_enabled_raw) = req.get_data("ocsp_enabled") { + cert_entry.ocsp_enabled = ocsp_enabled_raw.as_bool().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(ocsp_servers_override_raw) = req.get_data("ocsp_servers_override") { + cert_entry.ocsp_servers_override = + ocsp_servers_override_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(ocsp_fail_open_raw) = req.get_data("ocsp_fail_open") { + cert_entry.ocsp_fail_open = ocsp_fail_open_raw.as_bool().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(ocsp_query_all_servers_raw) = req.get_data("ocsp_query_all_servers") { + cert_entry.ocsp_query_all_servers = + ocsp_query_all_servers_raw.as_bool().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(display_name_raw) = req.get_data("display_name") { + cert_entry.display_name = display_name_raw.as_str().ok_or(RvError::ErrRequestFieldInvalid)?.to_string(); + } + + if let Ok(allowed_names_raw) = req.get_data("allowed_names") { + cert_entry.allowed_names = + allowed_names_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(allowed_common_names_raw) = req.get_data("allowed_common_names") { + cert_entry.allowed_common_names = + allowed_common_names_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(allowed_dns_sans_raw) = req.get_data("allowed_dns_sans") { + cert_entry.allowed_dns_sans = + allowed_dns_sans_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(allowed_email_sans_raw) = req.get_data("allowed_email_sans") { + cert_entry.allowed_email_sans = + allowed_email_sans_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(allowed_uri_sans_raw) = req.get_data("allowed_uri_sans") { + cert_entry.allowed_uri_sans = + allowed_uri_sans_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(allowed_organizational_units_raw) = req.get_data("allowed_organizational_units") { + cert_entry.allowed_organizational_units = + allowed_organizational_units_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(required_extensions_raw) = req.get_data("required_extensions") { + cert_entry.required_extensions = + required_extensions_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + if let Ok(allowed_metadata_extensions_raw) = req.get_data("allowed_metadata_extensions") { + cert_entry.allowed_metadata_extensions = + allowed_metadata_extensions_raw.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + } + + let old_token_policies = cert_entry.token_policies.clone(); + let old_token_period = cert_entry.token_period.clone(); + let old_token_ttl = cert_entry.token_ttl.clone(); + let old_token_max_ttl = cert_entry.token_max_ttl.clone(); + let old_token_bound_cidrs = cert_entry.token_bound_cidrs.clone(); + + cert_entry.token_params.parse_token_fields(req)?; + + if old_token_policies != cert_entry.token_policies { + cert_entry.policies = cert_entry.token_policies.clone(); + } else if let Ok(policies_value) = req.get_data("policies") { + let policies = policies_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + cert_entry.policies = policies.clone(); + cert_entry.token_policies = policies; + } + + if old_token_period != cert_entry.token_period { + cert_entry.period = cert_entry.token_period.clone(); + } else if let Ok(period_value) = req.get_data("period") { + let period = period_value.as_duration().ok_or(RvError::ErrRequestFieldInvalid)?; + cert_entry.period = period.clone(); + cert_entry.token_period = period; + } + + if old_token_ttl != cert_entry.token_ttl { + cert_entry.ttl = cert_entry.token_ttl.clone(); + } else if let Ok(ttl_value) = req.get_data("ttl") { + let ttl = ttl_value.as_duration().ok_or(RvError::ErrRequestFieldInvalid)?; + cert_entry.ttl = ttl.clone(); + cert_entry.token_ttl = ttl; + } else if let Ok(lease_value) = req.get_data("lease") { + let lease = lease_value.as_u64().ok_or(RvError::ErrRequestFieldInvalid)?; + cert_entry.ttl = Duration::from_secs(lease); + cert_entry.token_ttl = cert_entry.ttl.clone(); + } + + if old_token_max_ttl != cert_entry.token_max_ttl { + cert_entry.max_ttl = cert_entry.token_max_ttl.clone(); + } else if let Ok(max_ttl_value) = req.get_data("max_ttl") { + let max_ttl = max_ttl_value.as_duration().ok_or(RvError::ErrRequestFieldInvalid)?; + cert_entry.max_ttl = max_ttl.clone(); + cert_entry.token_max_ttl = max_ttl; + } + + if old_token_bound_cidrs != cert_entry.token_bound_cidrs { + cert_entry.bound_cidrs = cert_entry.token_bound_cidrs.clone(); + } else if let Ok(bound_cidrs_value) = req.get_data("bound_cidrs") { + let bound_cidrs = bound_cidrs_value.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + cert_entry.bound_cidrs = bound_cidrs + .iter() + .map(|s| SockAddrMarshaler::from_str(s)) + .collect::, _>>()?; + cert_entry.token_bound_cidrs = cert_entry.bound_cidrs.clone(); + } + + if cert_entry.display_name == "" { + cert_entry.display_name = name.clone(); + } + + //TODO: TTL check + + //If the certificate is not a CA cert, then ensure that x509.ExtKeyUsageClientAuth is set + let cert = &cert_entry.certificate[0]; + if !is_ca_cert(cert) && has_x509_ext_key_usage(cert) && !has_x509_ext_key_usage_flag(cert, XKU_SSL_CLIENT) { + return Err(rv_error_response!( + "nonCA certificates should have TLS client authentication set as an extended key usage" + )); + } + + self.set_cert(req, &name, &cert_entry)?; + + Ok(None) + } + + pub fn delete_cert(&self, _backend: &dyn Backend, req: &Request) -> Result, RvError> { + let name = req.get_data_as_str("name")?.to_lowercase(); + + req.storage_delete(format!("cert/{}", name).as_str())?; + Ok(None) + } + + pub fn list_cert(&self, _backend: &dyn Backend, req: &Request) -> Result, RvError> { + let certs = req.storage_list(format!("cert/").as_str())?; + let resp = Response::list_response(&certs); + Ok(Some(resp)) + } +} diff --git a/src/modules/credential/cert/path_config.rs b/src/modules/credential/cert/path_config.rs new file mode 100644 index 0000000..8d4280a --- /dev/null +++ b/src/modules/credential/cert/path_config.rs @@ -0,0 +1,122 @@ +use std::{collections::HashMap, sync::Arc}; + +use serde::{Deserialize, Serialize}; + +use super::{CertBackend, CertBackendInner}; +use crate::{ + context::Context, + errors::RvError, + logical::{Backend, Field, FieldType, Operation, Path, PathOperation, Request, Response}, + new_fields, new_fields_internal, new_path, new_path_internal, + storage::StorageEntry, +}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + pub disable_binding: bool, + pub enable_identity_alias_metadata: bool, + pub ocsp_cache_size: i64, +} + +impl CertBackend { + pub fn config_path(&self) -> Path { + let cert_backend_ref1 = Arc::clone(&self.inner); + let cert_backend_ref2 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"config", + fields: { + "disable_binding": { + field_type: FieldType::Bool, + default: false, + description: r#"If set, during renewal, skips the matching of presented client identity with the client identity used during login. Defaults to false."# + }, + "enable_identity_alias_metadata": { + field_type: FieldType::Bool, + default: false, + description: r#"If set, metadata of the certificate including the metadata corresponding to allowed_metadata_extensions will be stored in the alias. Defaults to false."# + }, + "ocsp_cache_size": { + field_type: FieldType::Int, + default: 100, + description: "The size of the in memory OCSP response cache, shared by all configured certs" + } + }, + operations: [ + {op: Operation::Read, handler: cert_backend_ref1.read_config}, + {op: Operation::Write, handler: cert_backend_ref2.write_config} + ], + help: r#" +This endpoint allows you to create, read, update, and delete trusted certificates +that are allowed to authenticate. + +Deleting a certificate will not revoke auth for prior authenticated connections. +To do this, do a revoke on "login". If you don'log need to revoke login immediately, +then the next renew will cause the lease to expire. + "# + }); + + path + } +} + +impl CertBackendInner { + pub fn get_config(&self, req: &Request) -> Result, RvError> { + let storage_entry = req.storage_get("config")?; + if storage_entry.is_none() { + return Ok(Some(Config::default())); + } + + let entry = storage_entry.unwrap(); + let config: Config = serde_json::from_slice(entry.value.as_slice())?; + Ok(Some(config)) + } + + pub fn set_config(&self, req: &mut Request, config: &Config) -> Result<(), RvError> { + let entry = StorageEntry::new("config", config)?; + + req.storage_put(&entry) + } + + pub fn read_config(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let config = self.get_config(req)?; + if config.is_none() { + return Ok(None); + } + + let cfg = config.unwrap(); + let cfg_data = serde_json::to_value(&cfg)?; + + Ok(Some(Response::data_response(Some(cfg_data.as_object().unwrap().clone())))) + } + + pub fn write_config(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let config = self.get_config(req)?; + if config.is_none() { + return Ok(None); + } + + let mut cfg = config.unwrap(); + + if let Ok(disable_binding_raw) = req.get_data("disable_binding") { + cfg.disable_binding = disable_binding_raw.as_bool().unwrap(); + } + + if let Ok(enable_identity_alias_metadata_raw) = req.get_data("enable_identity_alias_metadata") { + cfg.enable_identity_alias_metadata = enable_identity_alias_metadata_raw.as_bool().unwrap(); + } + + if let Ok(ocsp_cache_size_raw) = req.get_data("ocsp_cache_size") { + let ocsp_cache_size = ocsp_cache_size_raw.as_i64().unwrap(); + if ocsp_cache_size < 2 { + log::error!("invalid cache size, must be >= 2 and <= max_cache_size"); + return Err(RvError::ErrRequestInvalid); + } + cfg.ocsp_cache_size = ocsp_cache_size; + } + + self.set_config(req, &cfg)?; + + Ok(None) + } +} diff --git a/src/modules/credential/cert/path_crls.rs b/src/modules/credential/cert/path_crls.rs new file mode 100644 index 0000000..35e428f --- /dev/null +++ b/src/modules/credential/cert/path_crls.rs @@ -0,0 +1,284 @@ +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use openssl::{bn::BigNum, x509::X509Crl}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use super::{path_config::Config, CertBackend, CertBackendInner}; +use crate::{ + context::Context, + errors::RvError, + logical::{Backend, Field, FieldType, Operation, Path, PathOperation, Request, Response}, + new_fields, new_fields_internal, new_path, new_path_internal, + storage::StorageEntry, + utils::{deserialize_duration, serialize_duration}, +}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RevokedSerialInfo; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CDPInfo { + pub url: String, + #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] + pub valid_until: Duration, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CRLInfo { + pub cdp: Option, + pub serials: HashMap, +} + +impl CertBackend { + pub fn crl_path(&self) -> Path { + let cert_backend_ref1 = Arc::clone(&self.inner); + let cert_backend_ref2 = Arc::clone(&self.inner); + let cert_backend_ref3 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"crls/(?P\w[\w-]+\w)", + fields: { + "name": { + field_type: FieldType::Str, + required: true, + description: "The name of the certificate." + }, + "crl": { + field_type: FieldType::Str, + description: r#"The public CRL that should be trusted to attest to certificates' validity statuses. + May be DER or PEM encoded. Note: the expiration time + is ignored; if the CRL is no longer valid, delete it + using the same name as specified here."# + }, + "url": { + field_type: FieldType::Str, + description: "The URL of a CRL distribution point. Only one of 'crl' or 'url' parameters should be specified." + } + }, + operations: [ + {op: Operation::Read, handler: cert_backend_ref1.read_crl}, + {op: Operation::Write, handler: cert_backend_ref2.write_crl}, + {op: Operation::Delete, handler: cert_backend_ref3.delete_crl} + ], + help: r#" +This endpoint allows you to list, create, read, update, and delete the Certificate +Revocation Lists checked during authentication, and/or CRL Distribution Point +URLs. + +When any CRLs are in effect, any login will check the trust chains sent by a +client against the submitted or retrieved CRLs. Any chain containing a serial number revoked +by one or more of the CRLs causes that chain to be marked as invalid for the +authentication attempt. Conversely, *any* valid chain -- that is, a chain +in which none of the serials are revoked by any CRL -- allows authentication. +This allows authentication to succeed when interim parts of one chain have been +revoked; for instance, if a certificate is signed by two intermediate CAs due to +one of them expiring. + "# + }); + + path + } + + pub fn crl_list_path(&self) -> Path { + let cert_backend_ref = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"crls/?$", + operations: [ + {op: Operation::List, handler: cert_backend_ref.list_crl} + ], + help: r#" +This endpoint allows you to list, create, read, update, and delete the Certificate +Revocation Lists checked during authentication, and/or CRL Distribution Point +URLs. + +When any CRLs are in effect, any login will check the trust chains sent by a +client against the submitted or retrieved CRLs. Any chain containing a serial number revoked +by one or more of the CRLs causes that chain to be marked as invalid for the +authentication attempt. Conversely, *any* valid chain -- that is, a chain +in which none of the serials are revoked by any CRL -- allows authentication. +This allows authentication to succeed when interim parts of one chain have been +revoked; for instance, if a certificate is signed by two intermediate CAs due to +one of them expiring. + "# + }); + + path + } +} + +impl CertBackendInner { + pub fn get_crl(&self, req: &mut Request) -> Result, RvError> { + let storage_entry = req.storage_get("crls")?; + if storage_entry.is_none() { + return Ok(None); + } + + let entry = storage_entry.unwrap(); + let config: Config = serde_json::from_slice(entry.value.as_slice())?; + Ok(Some(config)) + } + + pub fn set_crl( + &self, + req: &mut Request, + x509crl: Option, + name: &str, + cdp: Option, + ) -> Result<(), RvError> { + self.update_crl_cache(req)?; + + let mut crl_info = CRLInfo { cdp, ..Default::default() }; + + if let Some(crl) = x509crl { + if let Some(revoked_stack) = crl.get_revoked() { + for revoked in revoked_stack.iter() { + let serial = revoked.serial_number().to_bn()?; + let serial_str = serial.to_dec_str()?; + crl_info.serials.insert(serial_str.to_lowercase(), RevokedSerialInfo {}); + } + } + } + + let entry = StorageEntry::new(format!("crls/{}", name).as_str(), &crl_info)?; + req.storage_put(&entry)?; + + self.crls.insert(name.to_string(), crl_info); + + Ok(()) + } + + pub fn fetch_crl(&self, req: &mut Request, name: &str, crl: CRLInfo) -> Result<(), RvError> { + if crl.cdp.is_none() { + return Err(RvError::ErrRequestInvalid); + } + + let url = crl.cdp.as_ref().unwrap().url.as_str(); + let body: String = ureq::get(url).call()?.into_string()?; + + let x509crl = X509Crl::from_pem(body.as_bytes())?; + self.set_crl(req, Some(x509crl), name, crl.cdp)?; + Ok(()) + } + + pub fn list_crl(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let crls = req.storage_list("crls/")?; + let resp = Response::list_response(&crls); + Ok(Some(resp)) + } + + pub fn read_crl(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let name = req.get_data_as_str("name")?.to_lowercase(); + if name == "" { + return Err(RvError::ErrRequestNoDataField); + } + + self.update_crl_cache(req)?; + + let crl = self.crls.get(&name); + if crl.is_none() { + log::error!("no such CRL {}", name); + return Err(RvError::ErrRequestInvalid); + }; + let crl_info = crl.unwrap(); + + let crl_data = serde_json::to_value(&*crl_info)?; + + Ok(Some(Response::data_response(Some(crl_data.as_object().unwrap().clone())))) + } + + pub fn write_crl(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let name = req.get_data_as_str("name")?.to_lowercase(); + if name == "" { + return Err(RvError::ErrRequestNoDataField); + } + + if let Ok(crl_value) = req.get_data("crl") { + let crl = crl_value.as_str().ok_or(RvError::ErrRequestFieldInvalid)?; + let x509crl = X509Crl::from_pem(crl.as_bytes())?; + self.set_crl(req, Some(x509crl), &name, None)?; + } else if let Ok(url_value) = req.get_data("url") { + let url = url_value.as_str().ok_or(RvError::ErrRequestFieldInvalid)?; + if url == "" { + return Err(RvError::ErrRequestInvalid); + } + let _ = Url::parse(url)?; + let cdp_info = CDPInfo { url: url.to_string(), ..Default::default() }; + let crl_info = CRLInfo { cdp: Some(cdp_info), ..Default::default() }; + + self.fetch_crl(req, &name, crl_info)?; + } else { + return Err(RvError::ErrRequestNoDataField); + } + + Ok(None) + } + + pub fn delete_crl(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let name = req.get_data_as_str("name")?.to_lowercase(); + if name == "" { + return Err(RvError::ErrRequestNoDataField); + } + + self.update_crl_cache(req)?; + + if self.crls.get(&name).is_none() { + log::error!("no such CRL {}", name); + return Err(RvError::ErrRequestInvalid); + } + + req.storage_delete(format!("crls/{}", name.to_lowercase()).as_str())?; + + self.crls.remove(&name); + + Ok(None) + } + + pub fn find_serial_in_crls(&self, serial: BigNum) -> Result, RvError> { + let serial_str = serial.to_dec_str()?; + let mut ret: HashMap = HashMap::new(); + for item in self.crls.iter() { + let crl = item.value(); + if let Some(info) = crl.serials.get(&serial_str.to_lowercase()) { + ret.insert(item.key().clone(), info.clone()); + } + } + + Ok(ret) + } + + fn update_crl_cache(&self, req: &Request) -> Result<(), RvError> { + if !self.crls.is_empty() { + return Ok(()); + } + + let keys = req.storage_list("crls/")?; + if keys.is_empty() { + return Ok(()); + } + + for key in &keys { + let entry = match req.storage_get(&format!("crls/{}", key)) { + Ok(None) => continue, + Ok(Some(entry)) => entry, + Err(err) => { + self.crls.clear(); + return Err(err); + } + }; + + let crl_info: CRLInfo = match serde_json::from_slice(entry.value.as_slice()) { + Ok(crl_info) => crl_info, + Err(err) => { + self.crls.clear(); + return Err(RvError::Serde { source: err }); + } + }; + + self.crls.insert(key.to_string(), crl_info); + } + + Ok(()) + } +} diff --git a/src/modules/credential/cert/path_login.rs b/src/modules/credential/cert/path_login.rs new file mode 100644 index 0000000..774fe37 --- /dev/null +++ b/src/modules/credential/cert/path_login.rs @@ -0,0 +1,642 @@ +use std::{collections::HashMap, sync::Arc}; + +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use foreign_types::ForeignType; +use glob::Pattern; +use openssl::{ + nid::Nid, + stack::Stack, + asn1::{Asn1Time, Asn1OctetString}, + x509::{ + verify::X509VerifyFlags, + store::{X509Store, X509StoreBuilder}, + X509StoreContext, X509VerifyResult, X509, + }, +}; +use openssl_sys::{ + XKU_SSL_CLIENT, XKU_ANYEKU, X509_V_ERR_CERT_HAS_EXPIRED, + X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, + ASN1_STRING_get0_data, ASN1_STRING_length, OBJ_obj2txt, X509_EXTENSION_get_data, X509_EXTENSION_get_object, + X509_get_ext, X509_get_ext_count, +}; +use serde::{Deserialize, Serialize}; + +use super::{CertBackend, CertBackendInner, CertEntry}; +use crate::{ + context::Context, + errors::RvError, + logical::{Auth, Backend, Field, FieldType, Operation, Path, PathOperation, Request, Response}, + new_fields, new_fields_internal, new_path, new_path_internal, rv_error_response, rv_error_response_status, + utils::{ + self, + cert::{ + deserialize_vec_x509, is_ca_cert, serialize_vec_x509, has_x509_ext_key_usage, has_x509_ext_key_usage_flag, + }, + ocsp::{self, OcspConfig}, + cidr::remote_addr_is_ok, + sock_addr::SockAddr, + }, +}; + +#[derive(Debug, Deserialize, Serialize)] +struct Asn1StringData { + #[serde(rename = "value")] + value: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ParsedCert { + pub entry: CertEntry, + #[serde(serialize_with = "serialize_vec_x509", deserialize_with = "deserialize_vec_x509")] + pub certs: Vec, +} + +impl CertBackend { + pub fn login_path(&self) -> Path { + let cert_backend_ref1 = Arc::clone(&self.inner); + + let path = new_path!({ + pattern: r"login", + fields: { + "name": { + field_type: FieldType::Str, + required: true, + description: "The name of the certificate role to authenticate against." + } + }, + operations: [ + {op: Operation::Write, handler: cert_backend_ref1.login} + ] + }); + + path + } +} + +impl CertBackendInner { + pub fn login(&self, _backend: &dyn Backend, req: &mut Request) -> Result, RvError> { + let config = self.get_config(req)?; + if config.is_none() { + return Err(RvError::ErrCredentailNotConfig); + } + + if req.connection.is_none() { + return Err(rv_error_response!("tls connection required")); + } + + let conn = req.connection.as_ref().ok_or(RvError::ErrRequestNotReady)?; + + let client_cert = conn + .peer_tls_cert + .as_ref() + .filter(|cert| !cert.is_empty()) + .and_then(|cert| cert.first()) + .ok_or(rv_error_response!("no client certificate found"))?; + + let common_name = client_cert + .subject_name() + .entries_by_nid(Nid::COMMONNAME) + .next() + .map(|c| c.data().as_utf8().map(|s| s.to_owned())) + .transpose()? + .unwrap_or_else(|| "".into()); + + let serial_number = client_cert.serial_number().to_bn().and_then(|bn| bn.to_dec_str())?; + let sn = String::from_utf8_lossy(serial_number.as_bytes()).to_string(); + + let subject_key_id = client_cert.subject_key_id().map(|asn1_ref| asn1_ref.as_slice()).unwrap_or(b""); + let authority_key_id = client_cert.authority_key_id().map(|asn1_ref| asn1_ref.as_slice()).unwrap_or(b""); + + let skid_base64 = STANDARD.encode(subject_key_id); + let akid_base64 = STANDARD.encode(authority_key_id); + let skid_hex = utils::hex_encode_with_colon(subject_key_id); + let akid_hex = utils::hex_encode_with_colon(authority_key_id); + + let matched = self.verify_credentials(req)?; + + if !matched.entry.token_bound_cidrs.is_empty() { + let token_bound_cidrs: Vec> = matched.entry.token_bound_cidrs.iter().map(|s| s.sock_addr.clone()).collect(); + if !remote_addr_is_ok(&conn.peer_addr, &token_bound_cidrs) { + return Err(RvError::ErrPermissionDenied); + } + } + + let mut auth = Auth { display_name: matched.entry.display_name.clone(), ..Default::default() }; + + auth.metadata.insert("cert_name".into(), matched.entry.name.clone()); + auth.metadata.insert("common_name".into(), common_name); + auth.metadata.insert("serial_number".into(), sn); + auth.metadata.insert("subject_key_id".into(), skid_hex); + auth.metadata.insert("authority_key_id".into(), akid_hex); + + auth.metadata.extend(self.certificate_extensions_metadata(&client_cert, &matched)); + + auth.internal_data.insert("subject_key_id".into(), skid_base64); + auth.internal_data.insert("authority_key_id".into(), akid_base64); + + matched.entry.populate_token_auth(&mut auth); + + let resp = Response { auth: Some(auth), ..Response::default() }; + + Ok(Some(resp)) + } + + pub fn login_renew(&self, _backend: &dyn Backend, _req: &mut Request) -> Result, RvError> { + //TODO + return Err(rv_error_response_status!(502, "TODO")); + } + + fn verify_credentials(&self, req: &Request) -> Result { + let peer_tls_cert = req.connection.as_ref() + .and_then(|conn| conn.peer_tls_cert.as_ref()) + .filter(|cert| !cert.is_empty()) + .ok_or_else(|| rv_error_response!("client certificate must be supplied"))?; + + let client_cert = &peer_tls_cert[0]; + + let cert_name = req.auth + .as_ref() + .and_then(|auth| auth.metadata.get("cert_name").cloned()) + .or_else(|| req.get_data("name").ok().and_then(|name| name.as_str().map(|s| s.to_string()))) + .unwrap_or_default(); + + + let (roots, trusted, trusted_non_ca, ocsp_config) = self.load_trusted_certs(req, &cert_name)?; + + let trusted_chains = self.validate_cert(&roots, peer_tls_cert)?; + + let mut ret_err = Vec::new(); + + for trust in trusted_non_ca.iter() { + let crt = &trust.certs[0]; + let crt_key_id = crt.authority_key_id(); + let client_key_id = client_cert.authority_key_id(); + if crt_key_id.is_none() || client_key_id.is_none() { + continue; + } + + if crt.serial_number() == client_cert.serial_number() && crt_key_id.unwrap().as_slice() == client_key_id.unwrap().as_slice() { + match self.matches_constraints(&client_cert, &trust.certs, trust, &ocsp_config) { + Ok(true) => return Ok(trust.clone()), + Err(e) => ret_err.push(e), + _ => {} + } + } + } + + if trusted_chains.is_empty() { + if !ret_err.is_empty() { + return Err(rv_error_response!( + &format!("invalid certificate or no client certificate supplied; additionally got errors during verification:: {:?}", ret_err) + )); + } + return Err(rv_error_response!("invalid certificate or no client certificate supplied")); + } + + for trust in trusted.iter() { + if trust.certs.iter().any(|crt| trusted_chains.contains(crt)) { + match self.matches_constraints(&client_cert, &trusted_chains, trust, &ocsp_config) { + Ok(true) => return Ok(trust.clone()), + Err(e) => ret_err.push(e), + _ => {} + } + } + } + + if !ret_err.is_empty() { + return Err(rv_error_response!( + &format!("no chain matching all constraints could be found for this login certificate; additionally got errors during verification: {:?}", ret_err) + )); + } + + return Err(rv_error_response!( + "no chain matching all constraints could be found for this login certificate" + )); + } + + fn load_trusted_certs( + &self, + req: &Request, + cert_name: &str, + ) -> Result<(X509Store, Vec, Vec, OcspConfig), RvError> { + let names: Vec = if !cert_name.is_empty() { + vec![cert_name.to_string()] + } else { + req.storage_list("cert/")? + }; + + let mut trusted: Vec = Vec::new(); + let mut trusted_non_ca: Vec = Vec::new(); + let mut root_store_builder = X509StoreBuilder::new()?; + let mut ocsp_config: OcspConfig = Default::default(); + + root_store_builder.set_flags(X509VerifyFlags::PARTIAL_CHAIN)?; + + for name in names.iter() { + if let Some(entry) = self.get_cert(req, name.trim_start_matches("cert/"))? { + if entry.certificate.is_empty() { + log::error!("failed to parse certificate, name: {}", name); + continue; + } + + if entry.ocsp_enabled { + ocsp_config.enable = true; + ocsp_config.servers_override.extend(entry.ocsp_servers_override.iter().cloned()); + ocsp_config.failure_mode = if entry.ocsp_fail_open { + ocsp::FailureMode::FailOpenTrue + } else { + ocsp::FailureMode::FailOpenFalse + }; + ocsp_config.query_all_servers |= entry.ocsp_query_all_servers; + } + + let mut certs = entry.certificate.clone(); + certs.extend(entry.ocsp_ca_certificates.clone()); + + if is_ca_cert(&certs[0]) { + for cert in certs.iter() { + root_store_builder.add_cert(cert.clone())?; + } + trusted.push(ParsedCert { entry, certs }); + } else { + trusted_non_ca.push(ParsedCert { entry, certs }); + } + } + } + + Ok((root_store_builder.build(), trusted, trusted_non_ca, ocsp_config)) + } + + fn validate_cert(&self, roots: &X509Store, peer_certs: &[X509]) -> Result, RvError> { + if peer_certs.is_empty() { + return Ok(Vec::new()); + } + let mut stack = Stack::::new()?; + peer_certs.iter().skip(1).try_for_each(|crt| stack.push(crt.clone()))?; + + let mut context = X509StoreContext::new()?; + let (verified_res, verified_error, verified_chains) = context.init(roots, &peer_certs[0], &stack, |ctx| { + let ret = ctx.verify_cert()?; + let verified_chains: Vec = ctx.chain() + .map(|chain| chain.iter().map(|crt| crt.to_owned()) + .filter(|crt| !has_x509_ext_key_usage(crt) || + has_x509_ext_key_usage_flag(crt, XKU_ANYEKU | XKU_SSL_CLIENT)) + .collect()) + .unwrap_or_else(Vec::new); + + Ok((ret, ctx.error(), verified_chains)) + })?; + + if !verified_res { + return match verified_error.as_raw() { + X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT | X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY => { + if peer_certs[0].not_after() <= Asn1Time::days_from_now(0)? { + Err(rv_error_response!(& unsafe {X509VerifyResult::from_raw(X509_V_ERR_CERT_HAS_EXPIRED).to_string() })) + } else { + Ok(Vec::new()) + } + }, + _ => Err(rv_error_response!(&verified_error.to_string())), + }; + } + + Ok(verified_chains) + } + + fn matches_constraints( + &self, + client_cert: &X509, + trusted_chain: &[X509], + config: &ParsedCert, + ocsp_config: &ocsp::OcspConfig, + ) -> Result { + let mut ret = !self.check_for_chain_in_crls(trusted_chain) + && self.matches_names(client_cert, config) + && self.matches_common_name(client_cert, config) + && self.matches_dns_sans(client_cert, config) + && self.matches_email_sans(client_cert, config) + && self.matches_uri_sans(client_cert, config) + && self.matches_organizational_units(client_cert, config) + && self.matches_certificate_extensions(client_cert, config); + + if config.entry.ocsp_enabled { + let ocsp_ret = self.check_for_cert_in_ocsp(client_cert, trusted_chain, ocsp_config)?; + ret = ret && ocsp_ret; + } + + return Ok(ret); + } + + fn matches_names(&self, client_cert: &X509, config: &ParsedCert) -> bool { + if config.entry.allowed_names.is_empty() { + return true; + } + + let common_name = match client_cert.subject_name().entries_by_nid(Nid::COMMONNAME).next() { + Some(entry) => match entry.data().as_utf8() { + Ok(cn_utf8) => cn_utf8.to_string(), + Err(_) => return false, + }, + None => return false, + }; + + let subject_alt_names = client_cert.subject_alt_names(); + + for allowed_name in &config.entry.allowed_names { + match Pattern::new(allowed_name) { + Ok(pattern) => { + if pattern.matches(&common_name) { + return true; + } + + if let Some(sans) = &subject_alt_names { + for san in sans { + if let Some(dnsname) = san.dnsname() { + if pattern.matches(dnsname) { + return true; + } + } + if let Some(email) = san.email() { + if pattern.matches(email) { + return true; + } + } + } + } + } + Err(_) => return false, + } + } + + false + } + + fn matches_common_name(&self, client_cert: &X509, config: &ParsedCert) -> bool { + if config.entry.allowed_common_names.is_empty() { + return true; + } + + let common_name = match client_cert.subject_name().entries_by_nid(Nid::COMMONNAME).next() { + Some(entry) => match entry.data().as_utf8() { + Ok(cn_utf8) => cn_utf8.to_string(), + Err(_) => return false, + }, + None => return false, + }; + + for allowed_common_name in &config.entry.allowed_common_names { + match Pattern::new(allowed_common_name) { + Ok(pattern) => { + if pattern.matches(&common_name) { + return true; + } + } + Err(_) => return false, + } + } + + false + } + + fn matches_dns_sans(&self, client_cert: &X509, config: &ParsedCert) -> bool { + if config.entry.allowed_dns_sans.is_empty() { + return true; + } + + let subject_alt_names = client_cert.subject_alt_names(); + + for allowed_dns in &config.entry.allowed_dns_sans { + match Pattern::new(allowed_dns) { + Ok(pattern) => { + if let Some(sans) = &subject_alt_names { + for san in sans { + if let Some(dnsname) = san.dnsname() { + if pattern.matches(dnsname) { + return true; + } + } + } + } + } + Err(_) => return false, + } + } + + false + } + + fn matches_email_sans(&self, client_cert: &X509, config: &ParsedCert) -> bool { + if config.entry.allowed_email_sans.is_empty() { + return true; + } + + let subject_alt_names = client_cert.subject_alt_names(); + + for allowed_email in config.entry.allowed_email_sans.iter() { + match Pattern::new(allowed_email) { + Ok(pattern) => { + if let Some(sans) = &subject_alt_names { + for san in sans { + if let Some(email) = san.email() { + if pattern.matches(email) { + return true; + } + } + } + } + } + Err(_) => return false, + } + } + + false + } + + fn matches_uri_sans(&self, client_cert: &X509, config: &ParsedCert) -> bool { + if config.entry.allowed_uri_sans.is_empty() { + return true; + } + + let subject_alt_names = client_cert.subject_alt_names(); + + for allowed_uri in &config.entry.allowed_uri_sans { + if let Ok(pattern) = Pattern::new(allowed_uri) { + if let Some(sans) = &subject_alt_names { + if sans.iter().any(|san| san.uri().map_or(false, |uri| pattern.matches(uri))) { + return true; + } + } + } else { + return false; + } + } + + false + } + + fn matches_organizational_units(&self, client_cert: &X509, config: &ParsedCert) -> bool { + if config.entry.allowed_organizational_units.is_empty() { + return true; + } + + let ou_name = match client_cert.subject_name().entries_by_nid(Nid::ORGANIZATIONALUNITNAME).next() { + Some(entry) => match entry.data().as_utf8() { + Ok(ou_utf8) => ou_utf8.to_string(), + Err(_) => return false, + }, + None => return false, + }; + + for allowed_ou in config.entry.allowed_organizational_units.iter() { + match Pattern::new(allowed_ou) { + Ok(pattern) => { + if pattern.matches(&ou_name) { + return true; + } + } + Err(_) => return false, + } + } + + false + } + + fn matches_certificate_extensions(&self, client_cert: &X509, config: &ParsedCert) -> bool { + if config.entry.required_extensions.is_empty() { + return true; + } + + let mut client_ext_map: HashMap = HashMap::new(); + let mut hex_ext_map: HashMap = HashMap::new(); + + unsafe { + let ext_count = X509_get_ext_count(client_cert.as_ptr()); + for i in 0..ext_count { + let ext = X509_get_ext(client_cert.as_ptr(), i); + + let obj = X509_EXTENSION_get_object(ext); + let mut oid_buf = [0; 128]; + let oid_len = OBJ_obj2txt(oid_buf.as_mut_ptr() as *mut _, oid_buf.len() as i32, obj, 1); + let oid = std::str::from_utf8(&oid_buf[..oid_len as usize]).unwrap(); + let ext_data = X509_EXTENSION_get_data(ext); + let ext_value = ASN1_STRING_get0_data(ext_data as *mut _); + let ext_len = ASN1_STRING_length(ext_data as *mut _); + let ext_slice = std::slice::from_raw_parts(ext_value, ext_len as usize); + let ext_str = Asn1OctetString::new_from_bytes(ext_slice) + .map(|ext_der| String::from_utf8_lossy(ext_der.as_slice()).to_string()) + .unwrap_or_else(|_| String::new()); + client_ext_map.insert(oid.to_string(), ext_str); + hex_ext_map.insert(oid.to_string(), hex::encode(ext_slice)); + } + } + + for required_ext in config.entry.required_extensions.iter() { + let req_ext: Vec<&str> = required_ext.splitn(2, ':').collect(); + if req_ext.len() != 2 { + return false; + } + + if req_ext[0] == "hex" { + let req_hex_ext: Vec<&str> = req_ext[1].splitn(2, ':').collect(); + if req_hex_ext.len() != 2 { + return false; + } + + let is_match = hex_ext_map.get(req_hex_ext[0]) + .and_then(|client_ext_value| { + Pattern::new(&req_hex_ext[1].to_lowercase()) + .ok() + .filter(|pattern| pattern.matches(client_ext_value)) + }) + .is_some(); + + if !is_match { + return false; + } + } else { + let is_match = client_ext_map.get(req_ext[0]) + .and_then(|client_ext_value| { + Pattern::new(&req_ext[1]) + .ok() + .filter(|pattern| pattern.matches(client_ext_value)) + }) + .is_some(); + + if !is_match { + return false; + } + } + } + + return true; + } + + fn certificate_extensions_metadata(&self, client_cert: &X509, config: &ParsedCert) -> HashMap { + let mut metadata_map: HashMap = HashMap::new(); + if config.entry.allowed_metadata_extensions.is_empty() { + return metadata_map; + } + + let mut allowed_oid_map: HashMap = HashMap::new(); + + for oid_string in config.entry.allowed_metadata_extensions.iter() { + allowed_oid_map.insert(oid_string.clone(), oid_string.replace(".", "-")); + } + + unsafe { + let ext_count = X509_get_ext_count(client_cert.as_ptr()); + for i in 0..ext_count { + let ext = X509_get_ext(client_cert.as_ptr(), i); + + let obj = X509_EXTENSION_get_object(ext); + let mut oid_buf = [0; 128]; + let oid_len = OBJ_obj2txt(oid_buf.as_mut_ptr() as *mut _, oid_buf.len() as i32, obj, 1); + let oid = std::str::from_utf8(&oid_buf[..oid_len as usize]).unwrap(); + if let Some(metadata_key) = allowed_oid_map.get(oid) { + let ext_data = X509_EXTENSION_get_data(ext); + let ext_value = ASN1_STRING_get0_data(ext_data as *mut _); + let ext_len = ASN1_STRING_length(ext_data as *mut _); + let ext_slice = std::slice::from_raw_parts(ext_value, ext_len as usize); + let ext_str = Asn1OctetString::new_from_bytes(ext_slice) + .map(|ext_der| String::from_utf8_lossy(ext_der.as_slice()).to_string()) + .unwrap_or_else(|_| String::new()); + metadata_map.insert(metadata_key.clone(), ext_str); + } + } + } + + return metadata_map; + } + + fn check_for_cert_in_ocsp( + &self, + _client_cert: &X509, + chain: &[X509], + ocsp_config: &OcspConfig, + ) -> Result { + if !ocsp_config.enable || chain.len() < 2 { + return Ok(true); + } + + //TODO + //let err = self.ocsp_client.verify_leaf_certificate(client_cert, chain, ocsp_config)?; + return Ok(true); + } + + fn check_for_chain_in_crls(&self, chain: &[X509]) -> bool { + for cert in chain { + let serial = cert.serial_number().to_bn(); + if serial.is_err() { + return false; + } + + if let Ok(bad_crls) = self.find_serial_in_crls(serial.unwrap()) { + if !bad_crls.is_empty() { + return true; + } + } + } + + false + } +} diff --git a/src/modules/credential/mod.rs b/src/modules/credential/mod.rs index 5e7bf01..c8ec110 100644 --- a/src/modules/credential/mod.rs +++ b/src/modules/credential/mod.rs @@ -3,4 +3,5 @@ //! pub mod approle; +pub mod cert; pub mod userpass;