From 5737fdd50641aad880439c98eb5b095ff32f05b0 Mon Sep 17 00:00:00 2001 From: Jin Jiu Date: Wed, 18 Sep 2024 23:14:22 +0800 Subject: [PATCH] Test case optimization and bug fixing. 1. Add test framework TestHttpServer for testing HTTP interfaces to better test restful API interfaces. 2. Fix the bug of rustls parsing failure caused by incorrect version field in issued certificate. 3. Fix the bug of unable to obtain client certificate through HTTPS interface. --- Cargo.toml | 9 +- src/cli/command/server.rs | 4 + src/cli/config.rs | 1 - src/errors.rs | 37 + src/http/logical.rs | 19 +- src/http/mod.rs | 13 +- src/logical/auth.rs | 26 +- src/logical/backend.rs | 2 +- src/logical/lease.rs | 2 +- src/logical/request.rs | 2 +- src/logical/response.rs | 2 +- src/logical/secret.rs | 7 +- src/modules/auth/expiration.rs | 2 +- src/modules/auth/token_store.rs | 11 +- src/modules/credential/approle/mod.rs | 27 +- src/modules/credential/approle/path_role.rs | 84 ++- .../credential/approle/path_tidy_secret_id.rs | 6 +- src/modules/credential/approle/validation.rs | 2 +- src/modules/credential/userpass/mod.rs | 6 +- src/modules/pki/mod.rs | 20 +- src/modules/pki/path_roles.rs | 30 +- src/storage/barrier_aes_gcm.rs | 5 +- src/storage/barrier_view.rs | 5 +- src/storage/mod.rs | 3 +- src/storage/physical/file.rs | 9 +- src/test_utils.rs | 670 +++++++++++++++++- src/utils/cert.rs | 28 +- src/utils/key.rs | 2 +- src/utils/mod.rs | 2 +- src/utils/ocsp.rs | 5 +- src/utils/salt.rs | 2 +- src/utils/sock_addr.rs | 6 + src/utils/token_util.rs | 22 + 33 files changed, 938 insertions(+), 133 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11266cf..818914f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,21 +47,22 @@ delay_timer = "0.11.6" as-any = "0.3.1" pem = "3.0" chrono = "0.4" -zeroize = { version = "1.7.0", features= ["zeroize_derive"] } +zeroize = { version = "1.7.0", features = ["zeroize_derive"] } diesel = { version = "2.1.4", features = ["mysql", "r2d2"], optional = true } r2d2 = { version = "0.8.9", optional = true } r2d2-diesel = { version = "1.0.0", optional = true } bcrypt = "0.15" url = "2.5" -ureq = "2.9" +ureq = { version = "2.10", features = ["json"] } +rustls = "0.23" +rustls-pemfile = "2.1" glob = "0.3" -serde_asn1_der = "0.8" base64 = "0.22" ipnetwork = "0.20" blake2b_simd = "1.0" derive_more = "0.99.17" dashmap = "5.5" -tokio = "1.38" +tokio = { version = "1.40", features = ["rt-multi-thread", "macros"] } ctor = "0.2.8" better_default = "1.0.5" diff --git a/src/cli/command/server.rs b/src/cli/command/server.rs index bd52781..ddb8d3d 100644 --- a/src/cli/command/server.rs +++ b/src/cli/command/server.rs @@ -158,6 +158,10 @@ pub fn main(config_path: &str) -> Result<(), RvError> { builder.set_ciphersuites("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256")?; } + if !listener.tls_disable_client_certs { + builder.set_verify_callback(SslVerifyMode::PEER, |_, _| true); + } + if listener.tls_require_and_verify_client_cert { builder.set_verify_callback(SslVerifyMode::PEER | SslVerifyMode::FAIL_IF_NO_PEER_CERT, move |p, _x| { return p; diff --git a/src/cli/config.rs b/src/cli/config.rs index 22532b0..2b14b22 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -312,7 +312,6 @@ mod test { use std::{env, fs, io::prelude::*}; use super::*; - use crate::test_utils::TEST_DIR; fn write_file(path: &str, config: &str) -> Result<(), RvError> { diff --git a/src/errors.rs b/src/errors.rs index 76d292d..89e1d7d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -9,6 +9,7 @@ use std::{ }; use thiserror::Error; +use actix_web::http::StatusCode; #[derive(Error, Debug)] pub enum RvError { @@ -266,6 +267,21 @@ pub enum RvError { source: url::ParseError, }, + #[error("Some rustls error happened, {:?}", .source)] + RustlsError { + #[from] + source: rustls::Error, + }, + + #[error("Some rustls_pemfile error happened")] + RustlsPemFileError(rustls_pemfile::Error), + + #[error("Some string utf8 error happened, {:?}", .source)] + StringUtf8Error { + #[from] + source: std::string::FromUtf8Error, + }, + /// Database Errors Begin /// #[error("Database type is not support now. Please try postgressql or mysql again.")] @@ -297,6 +313,21 @@ pub enum RvError { ErrUnknown, } +impl RvError { + pub fn response_status(&self) -> StatusCode { + match self { + RvError::ErrRequestNoData + | RvError::ErrRequestNoDataField + | RvError::ErrRequestInvalid + | RvError::ErrRequestClientTokenMissing + | RvError::ErrRequestFieldNotFound + | RvError::ErrRequestFieldInvalid => StatusCode::BAD_REQUEST, + RvError::ErrPermissionDenied => StatusCode::FORBIDDEN, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + impl PartialEq for RvError { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -398,6 +429,12 @@ impl From>> for RvError { } } +impl From for RvError { + fn from(err: rustls_pemfile::Error) -> Self { + RvError::RustlsPemFileError(err) + } +} + #[macro_export] macro_rules! rv_error_response { ($message:expr) => { diff --git a/src/http/logical.rs b/src/http/logical.rs index 41ef429..6a9147a 100644 --- a/src/http/logical.rs +++ b/src/http/logical.rs @@ -83,17 +83,16 @@ async fn logical_request_handler( } let core = core.read()?; - let resp = core.handle_request(&mut r)?; - - if r.operation == Operation::Read && resp.is_none() { - return Ok(response_error(StatusCode::NOT_FOUND, "")); - } - - if resp.is_none() { - return Ok(response_ok(None, None)); + let res = core.handle_request(&mut r)?; + match res { + Some(resp) => response_logical(&resp, &r.path), + None => { + if matches!(r.operation, Operation::Read | Operation::List) { + return Ok(response_error(StatusCode::NOT_FOUND, "")); + } + Ok(response_ok(None, None)) + } } - - response_logical(&resp.unwrap(), &r.path) } fn response_logical(resp: &Response, path: &str) -> Result { diff --git a/src/http/mod.rs b/src/http/mod.rs index 417e72d..a7c3750 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -67,11 +67,19 @@ pub fn request_on_connect_handler(conn: &dyn Any, ext: &mut Extensions) { return; } - if let Some(cert_stack) = tls_stream.ssl().verified_chain() { + if let Some(cert_stack) = tls_stream.ssl().peer_cert_chain() { let certs: Vec = cert_stack.iter().map(X509Ref::to_owned).collect(); cert_chain = Some(certs); } + if let Some(cert) = tls_stream.ssl().peer_certificate() { + if let Some(ref mut chain) = cert_chain { + chain.push(cert); + } else { + cert_chain = Some(vec![cert]); + } + } + ext.insert(Connection { bind: socket.local_addr().unwrap(), peer: peer_addr.unwrap(), @@ -106,9 +114,10 @@ pub fn init_service(cfg: &mut web::ServiceConfig) { impl ResponseError for RvError { // builds the actual response to send back when an error occurs fn error_response(&self) -> HttpResponse { - let mut status = StatusCode::INTERNAL_SERVER_ERROR; + let mut status = self.response_status(); let text: String; if let RvError::ErrResponse(resp_text) = self { + status = StatusCode::from_u16(400).unwrap(); text = resp_text.clone(); } else if let RvError::ErrResponseStatus(status_code, resp_text) = self { status = StatusCode::from_u16(status_code.clone()).unwrap(); diff --git a/src/logical/auth.rs b/src/logical/auth.rs index fd21be6..63e3f61 100644 --- a/src/logical/auth.rs +++ b/src/logical/auth.rs @@ -1,6 +1,4 @@ -use std::{ - collections::HashMap, -}; +use std::collections::HashMap; use derive_more::{Deref, DerefMut}; use serde::{Deserialize, Serialize}; @@ -12,9 +10,31 @@ pub struct Auth { #[deref] #[deref_mut] pub lease: Lease, + + // ClientToken is the token that is generated for the authentication. + // This will be filled in by Vault core when an auth structure is returned. + // Setting this manually will have no effect. pub client_token: String, + + // DisplayName is a non-security sensitive identifier that is applicable to this Auth. + // It is used for logging and prefixing of dynamic secrets. For example, + // DisplayName may be "armon" for the github credential backend. If the client token + // is used to generate a SQL credential, the user may be "github-armon-uuid". + // This is to help identify the source without using audit tables. pub display_name: String, + + // Policies is the list of policies that the authenticated user is associated with. pub policies: Vec, + + // Indicates that the default policy should not be added by core when creating a token. + // The default policy will still be added if it's explicitly defined. + pub no_default_policy: bool, + + // InternalData is JSON-encodable data that is stored with the auth struct. + // This will be sent back during a Renew/Revoke for storing internal data used for those operations. pub internal_data: HashMap, + + // Metadata is used to attach arbitrary string-type metadata to an authenticated user. + // This metadata will be outputted into the audit log. pub metadata: HashMap, } diff --git a/src/logical/backend.rs b/src/logical/backend.rs index d6f5ffb..68865d8 100644 --- a/src/logical/backend.rs +++ b/src/logical/backend.rs @@ -259,9 +259,9 @@ mod test { use super::*; use crate::{ - test_utils::test_backend, logical::{field::FieldTrait, Field, FieldType, PathOperation}, new_fields, new_fields_internal, new_path, new_path_internal, new_secret, new_secret_internal, storage, + test_utils::test_backend, }; struct MyTest; diff --git a/src/logical/lease.rs b/src/logical/lease.rs index cd554e7..9394707 100644 --- a/src/logical/lease.rs +++ b/src/logical/lease.rs @@ -1,7 +1,7 @@ use std::time::{Duration, SystemTime}; -use serde::{Deserialize, Serialize}; use better_default::Default; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Lease { diff --git a/src/logical/request.rs b/src/logical/request.rs index a4970ac..330086d 100644 --- a/src/logical/request.rs +++ b/src/logical/request.rs @@ -1,8 +1,8 @@ use std::{collections::HashMap, sync::Arc}; +use better_default::Default; use serde_json::{Map, Value}; use tokio::task::JoinHandle; -use better_default::Default; use super::{Operation, Path}; use crate::{ diff --git a/src/logical/response.rs b/src/logical/response.rs index c9a88fe..d66621b 100644 --- a/src/logical/response.rs +++ b/src/logical/response.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; +use better_default::Default; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -use better_default::Default; use crate::{ errors::RvError, diff --git a/src/logical/secret.rs b/src/logical/secret.rs index bbe5f56..cbf1da6 100644 --- a/src/logical/secret.rs +++ b/src/logical/secret.rs @@ -1,11 +1,8 @@ -use std::{ - sync::Arc, - time::Duration, -}; +use std::{sync::Arc, time::Duration}; +use derive_more::{Deref, DerefMut}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use derive_more::{Deref, DerefMut}; use super::{lease::Lease, Backend, Request, Response}; use crate::errors::RvError; diff --git a/src/modules/auth/expiration.rs b/src/modules/auth/expiration.rs index 6ddb6cc..c7076d8 100644 --- a/src/modules/auth/expiration.rs +++ b/src/modules/auth/expiration.rs @@ -5,11 +5,11 @@ use std::{ time::{Duration, SystemTime}, }; +use better_default::Default; use delay_timer::prelude::*; use derive_more::Deref; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use better_default::Default; use super::TokenStore; use crate::{ diff --git a/src/modules/auth/token_store.rs b/src/modules/auth/token_store.rs index 95e24f0..763876d 100644 --- a/src/modules/auth/token_store.rs +++ b/src/modules/auth/token_store.rs @@ -1,9 +1,9 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; +use derive_more::Deref; use humantime::parse_duration; use lazy_static::lazy_static; use regex::Regex; -use derive_more::Deref; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -19,10 +19,11 @@ use crate::{ logical::{ Auth, Backend, Field, FieldType, Lease, LogicalBackend, Operation, Path, PathOperation, Request, Response, }, + rv_error_response, new_fields, new_fields_internal, new_logical_backend, new_logical_backend_internal, new_path, new_path_internal, router::Router, storage::{Storage, StorageEntry}, - utils::{generate_uuid, is_str_subset, sha1}, + utils::{generate_uuid, is_str_subset, sha1, policy::sanitize_policies}, }; const TOKEN_LOOKUP_PREFIX: &str = "id/"; @@ -662,6 +663,12 @@ impl Handler for TokenStore { auth.ttl = MAX_LEASE_DURATION_SECS; } + sanitize_policies(&mut auth.policies, !auth.no_default_policy); + + if auth.policies.contains(&"root".to_string()) { + return Err(rv_error_response!("auth methods cannot create root tokens")); + } + let mut te = TokenEntry { path: req.path.clone(), meta: auth.metadata.clone(), diff --git a/src/modules/credential/approle/mod.rs b/src/modules/credential/approle/mod.rs index 9ef6cfd..9e795ec 100644 --- a/src/modules/credential/approle/mod.rs +++ b/src/modules/credential/approle/mod.rs @@ -217,15 +217,10 @@ mod test { use crate::{ core::Core, logical::{field::FieldTrait, Operation, Request}, - test_utils::{test_rusty_vault_init, test_read_api, test_write_api, test_delete_api, test_mount_auth_api}, + test_utils::{test_delete_api, test_mount_auth_api, test_read_api, test_rusty_vault_init, test_write_api}, }; - pub fn test_read_role( - core: &Core, - token: &str, - path: &str, - role_name: &str, - ) -> Result, RvError> { + pub fn test_read_role(core: &Core, token: &str, path: &str, role_name: &str) -> Result, RvError> { let resp = test_read_api(core, token, format!("auth/{}/role/{}", path, role_name).as_str(), true); assert!(resp.is_ok()); resp @@ -470,13 +465,8 @@ mod test { .as_object() .unwrap() .clone(); - let resp = test_write_api( - core, - token, - format!("auth/{}/role/{}", path, role_name).as_str(), - true, - Some(data.clone()), - ); + let resp = + test_write_api(core, token, format!("auth/{}/role/{}", path, role_name).as_str(), true, Some(data.clone())); assert!(resp.is_ok()); // Get the role field @@ -503,13 +493,8 @@ mod test { // Update the role data["token_num_uses"] = Value::from(0); data["token_type"] = Value::from("batch"); - let resp = test_write_api( - core, - token, - format!("auth/{}/role/{}", path, role_name).as_str(), - true, - Some(data.clone()), - ); + let resp = + test_write_api(core, token, format!("auth/{}/role/{}", path, role_name).as_str(), true, Some(data.clone())); assert!(resp.is_ok()); // Get the role field diff --git a/src/modules/credential/approle/path_role.rs b/src/modules/credential/approle/path_role.rs index 34d01aa..e427c74 100644 --- a/src/modules/credential/approle/path_role.rs +++ b/src/modules/credential/approle/path_role.rs @@ -1,9 +1,9 @@ use std::{collections::HashMap, mem, sync::Arc, time::Duration}; +use better_default::Default; use derive_more::{Deref, DerefMut}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use better_default::Default; use super::{ validation::{create_hmac, verify_cidr_role_secret_id_subset, SecretIdStorageEntry}, @@ -2195,19 +2195,19 @@ mod test { use serde_json::{json, Value}; use super::{ - *, super::{ + test::{generate_secret_id, test_delete_role, test_login, test_write_role}, AppRoleModule, SECRET_ID_PREFIX, - test::{ - generate_secret_id, test_login, test_write_role, test_delete_role, - }, }, + *, }; use crate::{ logical::{Operation, Request}, modules::auth::expiration::MAX_LEASE_DURATION_SECS, storage::Storage, - test_utils::{test_rusty_vault_init, test_list_api, test_read_api, test_write_api, test_delete_api, test_mount_auth_api}, + test_utils::{ + test_delete_api, test_list_api, test_mount_auth_api, test_read_api, test_rusty_vault_init, test_write_api, + }, }; #[test] @@ -2559,7 +2559,8 @@ mod test { .as_object() .unwrap() .clone(); - let resp = test_write_api(&core, &root_token, "auth/approle/role/testrolename/secret-id/lookup", true, Some(data)); + let resp = + test_write_api(&core, &root_token, "auth/approle/role/testrolename/secret-id/lookup", true, Some(data)); assert!(resp.is_ok()); // Listing of secret IDs should work in case-insensitive manner @@ -2678,8 +2679,13 @@ mod test { .as_object() .unwrap() .clone(); - let resp = - test_write_api(&core, &root_token, "auth/approle/role/testrole1/secret-id", false, Some(secret_data.clone())); + let resp = test_write_api( + &core, + &root_token, + "auth/approle/role/testrole1/secret-id", + false, + Some(secret_data.clone()), + ); assert!(resp.is_err()); role_data["bound_cidr_list"] = Value::from("192.168.27.29/16,172.245.30.40/24,10.20.30.40/30"); @@ -2719,12 +2725,18 @@ mod test { .as_object() .unwrap() .clone(); - let resp = - test_write_api(&core, &root_token, "auth/approle/role/testrole1/secret-id", true, Some(secret_data.clone())); + let resp = test_write_api( + &core, + &root_token, + "auth/approle/role/testrole1/secret-id", + true, + Some(secret_data.clone()), + ); assert!(resp.is_ok()); secret_data["token_bound_cidrs"] = Value::from("127.0.0.1/24"); - let resp = test_write_api(&core, &root_token, "auth/approle/role/testrole1/secret-id", false, Some(secret_data)); + let resp = + test_write_api(&core, &root_token, "auth/approle/role/testrole1/secret-id", false, Some(secret_data)); assert!(resp.is_err()); } @@ -2816,13 +2828,23 @@ mod test { .as_object() .unwrap() .clone(); - let resp = - test_write_api(&core, &root_token, "auth/approle/role/testrole1/role-id", false, Some(role_id_data.clone())); + let resp = test_write_api( + &core, + &root_token, + "auth/approle/role/testrole1/role-id", + false, + Some(role_id_data.clone()), + ); assert!(resp.is_err()); role_id_data["role_id"] = Value::from("role-id-123"); - let resp = - test_write_api(&core, &root_token, "auth/approle/role/testrole2/role-id", false, Some(role_id_data.clone())); + let resp = test_write_api( + &core, + &root_token, + "auth/approle/role/testrole2/role-id", + false, + Some(role_id_data.clone()), + ); assert!(resp.is_err()); role_id_data["role_id"] = Value::from("role-id-2000"); @@ -3366,7 +3388,8 @@ mod test { .as_object() .unwrap() .clone(); - let _ = test_write_api(&core, &root_token, "auth/approle/role/role1/bind-secret-id", true, Some(req_data.clone())); + let _ = + test_write_api(&core, &root_token, "auth/approle/role/role1/bind-secret-id", true, Some(req_data.clone())); let resp = test_read_api(&core, &root_token, "auth/approle/role/role1/bind-secret-id", true); assert!(resp.is_ok()); @@ -3421,8 +3444,13 @@ mod test { .as_object() .unwrap() .clone(); - let _ = - test_write_api(&core, &root_token, "auth/approle/role/role1/secret-id-num-uses", true, Some(req_data.clone())); + let _ = test_write_api( + &core, + &root_token, + "auth/approle/role/role1/secret-id-num-uses", + true, + Some(req_data.clone()), + ); let resp = test_read_api(&core, &root_token, "auth/approle/role/role1/secret-id-num-uses", true); assert!(resp.is_ok()); @@ -3446,7 +3474,8 @@ mod test { .as_object() .unwrap() .clone(); - let _ = test_write_api(&core, &root_token, "auth/approle/role/role1/secret-id-ttl", true, Some(req_data.clone())); + let _ = + test_write_api(&core, &root_token, "auth/approle/role/role1/secret-id-ttl", true, Some(req_data.clone())); let resp = test_read_api(&core, &root_token, "auth/approle/role/role1/secret-id-ttl", true); assert!(resp.is_ok()); @@ -3472,7 +3501,8 @@ mod test { .as_object() .unwrap() .clone(); - let _ = test_write_api(&core, &root_token, "auth/approle/role/role1/token-num-uses", true, Some(req_data.clone())); + let _ = + test_write_api(&core, &root_token, "auth/approle/role/role1/token-num-uses", true, Some(req_data.clone())); let resp = test_read_api(&core, &root_token, "auth/approle/role/role1/token-num-uses", true); assert!(resp.is_ok()); @@ -3548,7 +3578,8 @@ mod test { .as_object() .unwrap() .clone(); - let _ = test_write_api(&core, &root_token, "auth/approle/role/role1/token-max-ttl", true, Some(req_data.clone())); + let _ = + test_write_api(&core, &root_token, "auth/approle/role/role1/token-max-ttl", true, Some(req_data.clone())); let resp = test_read_api(&core, &root_token, "auth/approle/role/role1/token-max-ttl", true); assert!(resp.is_ok()); @@ -3704,8 +3735,13 @@ mod test { .as_object() .unwrap() .clone(); - let _ = - test_write_api(&core, &root_token, "auth/approle/role/role1/token-bound-cidrs", true, Some(req_data.clone())); + let _ = test_write_api( + &core, + &root_token, + "auth/approle/role/role1/token-bound-cidrs", + true, + Some(req_data.clone()), + ); let resp = test_read_api(&core, &root_token, "auth/approle/role/role1/token-bound-cidrs", true); assert!(resp.is_ok()); diff --git a/src/modules/credential/approle/path_tidy_secret_id.rs b/src/modules/credential/approle/path_tidy_secret_id.rs index ff96a41..30fa689 100644 --- a/src/modules/credential/approle/path_tidy_secret_id.rs +++ b/src/modules/credential/approle/path_tidy_secret_id.rs @@ -243,9 +243,9 @@ impl AppRoleBackendInner { #[cfg(test)] mod test { use std::{ - thread, default::Default, sync::{Arc, Mutex}, + thread, time::{Duration, Instant}, }; @@ -254,13 +254,13 @@ mod test { use tokio::runtime::Builder; use super::{ - *, super::{path_role::RoleEntry, AppRoleModule}, + *, }; use crate::{ logical::{Operation, Request}, storage::{Storage, StorageEntry}, - test_utils::{test_rusty_vault_init, test_mount_auth_api}, + test_utils::{test_mount_auth_api, test_rusty_vault_init}, }; #[test] diff --git a/src/modules/credential/approle/validation.rs b/src/modules/credential/approle/validation.rs index f562379..7aa51bf 100644 --- a/src/modules/credential/approle/validation.rs +++ b/src/modules/credential/approle/validation.rs @@ -6,9 +6,9 @@ use std::{ time::{Duration, SystemTime}, }; +use better_default::Default; use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer}; use serde::{Deserialize, Serialize}; -use better_default::Default; use super::{AppRoleBackendInner, SECRET_ID_ACCESSOR_LOCAL_PREFIX, SECRET_ID_ACCESSOR_PREFIX, SECRET_ID_LOCAL_PREFIX}; use crate::{ diff --git a/src/modules/credential/userpass/mod.rs b/src/modules/credential/userpass/mod.rs index 6cd2a93..ac1ddaf 100644 --- a/src/modules/credential/userpass/mod.rs +++ b/src/modules/credential/userpass/mod.rs @@ -1,6 +1,4 @@ -use std::{ - sync::{Arc, RwLock}, -}; +use std::sync::{Arc, RwLock}; use as_any::Downcast; use derive_more::Deref; @@ -131,7 +129,7 @@ mod test { use crate::{ core::Core, logical::{Operation, Request}, - test_utils::{test_rusty_vault_init, test_read_api, test_write_api, test_delete_api, test_mount_auth_api}, + test_utils::{test_delete_api, test_mount_auth_api, test_read_api, test_rusty_vault_init, test_write_api}, }; fn test_write_user(core: &Core, token: &str, path: &str, username: &str, password: &str, ttl: i32) { diff --git a/src/modules/pki/mod.rs b/src/modules/pki/mod.rs index 1fbc679..f21ca70 100644 --- a/src/modules/pki/mod.rs +++ b/src/modules/pki/mod.rs @@ -148,7 +148,7 @@ mod test { use super::*; use crate::{ core::Core, - test_utils::{test_rusty_vault_init, test_read_api, test_write_api, test_delete_api, test_mount_api}, + test_utils::{test_delete_api, test_mount_api, test_read_api, test_rusty_vault_init, test_write_api}, }; fn config_ca(core: &Core, token: &str, path: &str) { @@ -166,12 +166,12 @@ mod test { assert!(resp.is_ok()); } - fn config_role(core: &Core, token: &str, path: &str, role_name: &str) { + fn config_role(core: &Core, token: &str, path: &str, role_name: &str, key_type: &str, key_bits: u32) { let role_data = json!({ "ttl": "60d", "max_ttl": "365d", - "key_type": "rsa", - "key_bits": 4096, + "key_type": key_type, + "key_bits": key_bits, "country": "CN", "province": "Beijing", "locality": "Beijing", @@ -183,7 +183,8 @@ mod test { .clone(); // config role - assert!(test_write_api(&core, token, format!("{}roles/{}", path, role_name).as_str(), true, Some(role_data)).is_ok()); + assert!(test_write_api(&core, token, format!("{}roles/{}", path, role_name).as_str(), true, Some(role_data)) + .is_ok()); } fn generate_root(core: &Core, token: &str, path: &str, exported: bool, key_type: &str, key_bits: u32, is_ok: bool) { @@ -422,7 +423,7 @@ x/+V28hUf8m8P2NxP5ALaDZagdaMfzjGZo3O3wDv33Cds0P5GMGQYnRXDxcZN/2L config_ca(&core, token, path); // config role - config_role(&core, token, path, role_name); + config_role(&core, token, path, role_name, "rsa", 4096); let resp = test_read_api(&core, token, &format!("{}roles/{}", path, role_name), true); assert!(resp.as_ref().unwrap().is_some()); @@ -460,7 +461,7 @@ x/+V28hUf8m8P2NxP5ALaDZagdaMfzjGZo3O3wDv33Cds0P5GMGQYnRXDxcZN/2L config_ca(&core, token, path); // config role - config_role(&core, token, path, role_name); + config_role(&core, token, path, role_name, "rsa", 4096); let dns_sans = vec!["test.com", "a.test.com", "b.test.com"]; let issue_data = json!({ @@ -557,7 +558,7 @@ x/+V28hUf8m8P2NxP5ALaDZagdaMfzjGZo3O3wDv33Cds0P5GMGQYnRXDxcZN/2L config_ca(&core, token, path); // config role - config_role(&core, token, path, role_name); + config_role(&core, token, path, role_name, "rsa", 4096); generate_root(&core, token, path, true, "rsa", 4096, true); generate_root(&core, token, path, false, "rsa", 4096, true); @@ -568,6 +569,7 @@ x/+V28hUf8m8P2NxP5ALaDZagdaMfzjGZo3O3wDv33Cds0P5GMGQYnRXDxcZN/2L } #[cfg(feature = "crypto_adaptor_tongsuo")] + #[test] fn test_pki_sm2_generate_root() { let (root_token, c) = test_rusty_vault_init("test_pki_generate_root"); let token = &root_token; @@ -582,7 +584,7 @@ x/+V28hUf8m8P2NxP5ALaDZagdaMfzjGZo3O3wDv33Cds0P5GMGQYnRXDxcZN/2L config_ca(&core, token, path); // config role - config_role(&core, token, path, role_name); + config_role(&core, token, path, role_name, "sm2", 256); generate_root(&core, token, path, true, "sm2", 256, true); generate_root(&core, token, path, false, "sm2", 256, true); diff --git a/src/modules/pki/path_roles.rs b/src/modules/pki/path_roles.rs index 1579e6c..e7827be 100644 --- a/src/modules/pki/path_roles.rs +++ b/src/modules/pki/path_roles.rs @@ -1,14 +1,14 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; +use better_default::Default; use humantime::parse_duration; use serde::{Deserialize, Serialize}; -use better_default::Default; use super::{util::DEFAULT_MAX_TTL, PkiBackend, PkiBackendInner}; use crate::{ context::Context, errors::RvError, - logical::{Backend, Field, FieldType, Operation, Path, PathOperation, Request, Response}, + logical::{Backend, Field, FieldType, field::FieldTrait, Operation, Path, PathOperation, Request, Response}, new_fields, new_fields_internal, new_path, new_path_internal, storage::StorageEntry, utils::{deserialize_duration, serialize_duration}, @@ -46,6 +46,8 @@ pub struct RoleEntry { pub use_csr_sans: bool, #[default(true)] pub use_csr_common_name: bool, + pub key_usage: Vec, + pub ext_key_usage: Vec, pub country: String, pub province: String, pub locality: String, @@ -176,6 +178,24 @@ The number of bits to use in the signature algorithm; accepts 256 for SHA-2-256, 384 for SHA-2-384, and 512 for SHA-2-512. defaults to 0 to automatically detect based on key length (SHA-2-256 for RSA keys, and matching the curve size for NIST P-Curves)."# }, + "key_usage": { + field_type: FieldType::CommaStringSlice, + default: "DigitalSignature,KeyAgreement,KeyEncipherment", + description: r#" +A comma-separated string or list of key usages (not extended key usages). +Valid values can be found at https://golang.org/pkg/crypto/x509/#KeyUsage +-- simply drop the "KeyUsage" part of the name. +To remove all key usages from being set, set this value to an empty list. See also RFC 5280 Section 4.2.1.3. + "# + }, + "ext_key_usage": { + field_type: FieldType::CommaStringSlice, + description: r#" +A comma-separated string or list of extended key usages. Valid values can be found at +https://golang.org/pkg/crypto/x509/#ExtKeyUsage -- simply drop the "ExtKeyUsage" part of the name. +To remove all key usages from being set, set this value to an empty list. See also RFC 5280 Section 4.2.1.12. + "# + }, "not_before_duration": { field_type: FieldType::Int, default: 30, @@ -372,6 +392,10 @@ impl PkiBackendInner { let use_csr_sans = req.get_data_or_default("use_csr_sans")?.as_bool().ok_or(RvError::ErrRequestFieldInvalid)?; let use_csr_common_name = req.get_data_or_default("use_csr_common_name")?.as_bool().ok_or(RvError::ErrRequestFieldInvalid)?; + let key_usage = + req.get_data_or_default("key_usage")?.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; + let ext_key_usage = + req.get_data_or_default("ext_key_usage")?.as_comma_string_slice().ok_or(RvError::ErrRequestFieldInvalid)?; let country = req.get_data_or_default("country")?.as_str().ok_or(RvError::ErrRequestFieldInvalid)?.to_string(); let province = req.get_data_or_default("province")?.as_str().ok_or(RvError::ErrRequestFieldInvalid)?.to_string(); @@ -408,6 +432,8 @@ impl PkiBackendInner { client_flag, use_csr_sans, use_csr_common_name, + key_usage, + ext_key_usage, country, province, locality, diff --git a/src/storage/barrier_aes_gcm.rs b/src/storage/barrier_aes_gcm.rs index 2a7ac7e..d7eeac1 100644 --- a/src/storage/barrier_aes_gcm.rs +++ b/src/storage/barrier_aes_gcm.rs @@ -316,10 +316,7 @@ impl AESGCMBarrier { #[cfg(test)] mod test { use super::{super::*, *}; - - use crate::{ - test_utils::test_backend, - }; + use crate::test_utils::test_backend; #[test] fn test_encrypt_decrypt() { diff --git a/src/storage/barrier_view.rs b/src/storage/barrier_view.rs index d13e89c..59d1727 100644 --- a/src/storage/barrier_view.rs +++ b/src/storage/barrier_view.rs @@ -106,10 +106,7 @@ mod test { use rand::{thread_rng, Rng}; use super::{super::*, *}; - - use crate::{ - test_utils::test_backend, - }; + use crate::test_utils::test_backend; #[test] fn test_new_barrier_view() { diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 50ca2fe..95510b9 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -91,7 +91,8 @@ pub mod test { use serde_json::Value; use crate::{ - test_utils::TEST_DIR, storage::{new_backend, Backend, BackendEntry}, + storage::{new_backend, Backend, BackendEntry}, + test_utils::TEST_DIR, }; #[test] diff --git a/src/storage/physical/file.rs b/src/storage/physical/file.rs index 28510be..a9e5445 100644 --- a/src/storage/physical/file.rs +++ b/src/storage/physical/file.rs @@ -137,13 +137,8 @@ impl FileBackend { #[cfg(test)] mod test { - use super::{ - super::super::test::{test_backend_curd, test_backend_list_prefix}, - }; - - use crate::{ - test_utils::test_backend, - }; + use super::super::super::test::{test_backend_curd, test_backend_list_prefix}; + use crate::test_utils::test_backend; #[test] fn test_file_backend() { diff --git a/src/test_utils.rs b/src/test_utils.rs index 1097e56..9b2ddb1 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,25 +1,310 @@ use std::{ - env, fs, collections::HashMap, default::Default, - time::{SystemTime, UNIX_EPOCH}, - sync::{Arc, RwLock}, + env, fs, thread, + io::prelude::*, + path::Path, + sync::{Arc, RwLock, Barrier}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; +use libc::c_int; -use serde_json::{json, Map, Value}; +use foreign_types::ForeignType; +use humantime::parse_duration; use lazy_static::lazy_static; +use serde_json::{json, Map, Value}; +use actix_web::{middleware, web, dev::Server, App, HttpResponse, HttpServer}; +use ureq::AgentBuilder; +use rustls::{ + ClientConfig, + RootCertStore, + pki_types::{ + CertificateDer, + PrivateKeyDer, + } +}; +use tokio::sync::oneshot; +use anyhow::format_err; +use openssl::{ + rsa::Rsa, + pkey::{PKey, Private}, + hash::MessageDigest, + x509::{ + extension::{ + AuthorityKeyIdentifier, BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, + SubjectKeyIdentifier, + }, + X509Extension, X509NameBuilder, X509Ref, X509, + }, + ssl::{SslAcceptor, SslFiletype, SslMethod, SslVersion, SslVerifyMode}, + asn1::{Asn1Time, Asn1Object, Asn1OctetString}, +}; use crate::{ - errors::RvError, + http, core::{Core, SealConfig}, + errors::RvError, logical::{Operation, Request, Response}, storage::{self, Backend}, + utils::cert::Certificate, + rv_error_response, }; lazy_static! { pub static ref TEST_DIR: &'static str = "rusty_vault_test"; } +#[derive(Debug, Clone)] +pub struct TestTlsConfig { + pub cert_path: String, + pub key_path: String, +} + +#[derive(Debug, Clone)] +pub struct TestTlsClientAuth { + pub ca_pem: String, + pub cert_pem: String, + pub key_pem: String, +} + +pub struct TestHttpServer { + pub name: String, + pub mount_path: String, + pub core: Arc>, + pub root_token: String, + pub ca_cert_pem: String, + pub ca_key_pem: String, + pub server_cert_pem: String, + pub server_key_pem: String, + pub tls_enable: bool, + pub listen_addr: String, + pub url_prefix: String, + pub stop_tx: Option>, + pub thread: Option>, +} + +impl TestHttpServer { + pub fn new(name: &str, tls_enable: bool) -> Self { + let barrier = Arc::new(Barrier::new(2)); + let (stop_tx, stop_rx) = oneshot::channel(); + let (root_token, core) = test_rusty_vault_init(name); + + let mut scheme = "http"; + let mut ca_cert_pem = "".into(); + let mut ca_key_pem = "".into(); + let mut server_cert_pem = "".into(); + let mut server_key_pem = "".into(); + let mut test_tls_config = None; + + if tls_enable { + (ca_cert_pem, ca_key_pem) = new_test_cert(true, true, true, "test-ca", None, None, None, None, None, None).unwrap(); + (server_cert_pem, server_key_pem) = new_test_cert(false, true, true, "localhost", Some("localhost"), Some("127.0.0.1"), + None, None, Some(ca_cert_pem.clone()), Some(ca_key_pem.clone())).unwrap(); + + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let test_certs_dir = env::temp_dir().join(format!("{}/certs/{}-{}", *TEST_DIR, name, now).as_str()); + let dir = test_certs_dir.to_string_lossy().into_owned(); + assert!(fs::create_dir_all(&test_certs_dir).is_ok()); + + let ca_path = format!("{}/ca.crt", dir); + let cert_path = format!("{}/server.crt", dir); + let key_path = format!("{}/key.pem", dir); + + let mut ca_file = fs::File::create(&ca_path).unwrap(); + assert!(ca_file.write_all(ca_cert_pem.as_bytes()).is_ok()); + + let mut cert_file = fs::File::create(&cert_path).unwrap(); + assert!(cert_file.write_all(server_cert_pem.as_bytes()).is_ok()); + + let mut key_file = fs::File::create(&key_path).unwrap(); + assert!(key_file.write_all(server_key_pem.as_bytes()).is_ok()); + + test_tls_config = Some(TestTlsConfig { + cert_path, + key_path, + }); + + scheme = "https"; + } + + let (server, listen_addr) = new_test_http_server(core.clone(), test_tls_config).unwrap(); + let server_thread = start_test_http_server(server, Arc::clone(&barrier), stop_rx); + + barrier.wait(); + + let url_prefix = format!("{}://{}/v1", scheme, listen_addr); + + Self { + name: name.to_string(), + core, + root_token, + tls_enable, + ca_cert_pem, + ca_key_pem, + server_cert_pem, + server_key_pem, + listen_addr, + url_prefix, + mount_path: "".into(), + stop_tx: Some(stop_tx), + thread: Some(server_thread), + } + } + + pub fn mount(&mut self, path: &str, mtype: &str) -> Result<(u16, Value), RvError> { + let data = json!({ + "type": mtype, + }) + .as_object() + .unwrap() + .clone(); + let (status, resp) = self.write(&format!("sys/mounts/{}", path), Some(data), None)?; + if status == 200 || status == 204 { + self.mount_path = path.into(); + } + + Ok((status, resp)) + } + + pub fn mount_auth(&mut self, path: &str, atype: &str) -> Result<(u16, Value), RvError> { + let data = json!({ + "type": atype, + }) + .as_object() + .unwrap() + .clone(); + let (status, resp) = self.write(&format!("sys/auth/{}", path), Some(data), None)?; + if status == 200 || status == 204 { + self.mount_path = path.into(); + } + + Ok((status, resp)) + } + + pub fn login(&self, path: &str, data: Option>, tls_client_auth: Option) -> Result<(u16, Value), RvError> { + self.request("POST", path, data, None, tls_client_auth) + } + + pub fn list(&self, path: &str, token: Option<&str>) -> Result<(u16, Value), RvError> { + self.request("LIST", path, None, token, None) + } + + pub fn read(&self, path: &str, token: Option<&str>) -> Result<(u16, Value), RvError> { + self.request("GET", path, None, token, None) + } + + pub fn write(&self, path: &str, data: Option>, token: Option<&str>) -> Result<(u16, Value), RvError> { + self.request("POST", path, data, token, None) + } + + pub fn delete(&self, path: &str, data: Option>, token: Option<&str>) -> Result<(u16, Value), RvError> { + self.request("DELETE", path, data, token, None) + } + + pub fn request(&self, method: &str, path: &str, data: Option>, token: Option<&str>, tls_client_auth: Option) -> Result<(u16, Value), RvError> { + let url = format!("{}/{}", self.url_prefix, path); + println!("request url: {}, method: {}", url, method); + let tk = token.unwrap_or(&self.root_token); + let mut req = if self.tls_enable { + // Create rustls ClientConfig + let tls_config; + if let Some(client_auth) = tls_client_auth { + let ca_pem = pem::parse(client_auth.ca_pem.as_bytes())?; + let ca_cert = CertificateDer::from_slice(ca_pem.contents()); + + let mut ca_store = RootCertStore::empty(); + ca_store.add(ca_cert)?; + + let mut client_certs = vec![]; + let mut cert_pem = client_auth.cert_pem.as_bytes(); + loop { + match rustls_pemfile::read_one_from_slice(cert_pem)? { + Some((rustls_pemfile::Item::X509Certificate(cert), rest)) => { + cert_pem = rest; + client_certs.push(cert.into()); + }, + None => break, + _ => return Err(rv_error_response!("client cert format invalid")), + } + } + + let client_key: PrivateKeyDer = match rustls_pemfile::read_one_from_slice(client_auth.key_pem.as_bytes())? { + Some((rustls_pemfile::Item::Pkcs1Key(key), _)) => PrivateKeyDer::Pkcs1(key), + Some((rustls_pemfile::Item::Pkcs8Key(key), _)) => PrivateKeyDer::Pkcs8(key), + _ => return Err(rv_error_response!("client key format invalid")), + }; + + + tls_config = ClientConfig::builder() + .with_root_certificates(ca_store) + .with_client_auth_cert(client_certs, client_key)?; + } else { + let cert_pem = pem::parse(self.ca_cert_pem.as_bytes())?; + let root_cert = CertificateDer::from_slice(cert_pem.contents()); + + // Configure the root certificate + let mut root_store = RootCertStore::empty(); + root_store.add(root_cert)?; + + tls_config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + } + + let agent = AgentBuilder::new() + .timeout_connect(Duration::from_secs(10)) + .timeout(Duration::from_secs(30)) + .tls_config(Arc::new(tls_config)) + .build(); + agent.request(&method.to_uppercase(), &url) + } else { + ureq::request(&method.to_uppercase(), &url) + }; + + req = req.set("Accept", "application/json"); + if !path.ends_with("/login") { + req = req.set("X-RustyVault-Token", tk); + } + + let response_result = if let Some(send_data) = data { + req.send_json(send_data) + } else { + req.call() + }; + + match response_result { + Ok(response) => { + let status = response.status(); + if status == 204 { + return Ok((status, json!(""))); + } + let json: Value = response.into_json()?; + return Ok((status, json)) + }, + Err(ureq::Error::Status(code, response)) => { + let json: Value = response.into_json()?; + return Ok((code, json)) + }, + Err(e) => { + println!("Request failed: {}", e); + return Err(RvError::UreqError { source: e }); + } + } + } +} + +impl Drop for TestHttpServer { + fn drop(&mut self) { + if let Some(tx) = self.stop_tx.take() { + tx.send(()).expect("Failed to send stop signal."); + } + + if let Some(thread) = self.thread.take() { + thread.join().expect("Failed to join thread."); + } + } +} + mod tests { use super::*; @@ -27,6 +312,7 @@ mod tests { fn init() { let dir = env::temp_dir().join(*TEST_DIR); let _ = fs::remove_dir_all(&dir); + let _ = rustls::crypto::ring::default_provider().install_default(); println!("create rusty_vault_test dir: {}", dir.to_string_lossy().into_owned()); assert!(fs::create_dir(&dir).is_ok()); } @@ -38,6 +324,303 @@ mod tests { } } +pub fn new_test_cert( + is_ca: bool, + client_auth: bool, + server_auth: bool, + common_name: &str, + dns_sans: Option<&str>, + ip_sans: Option<&str>, + uri_sans: Option<&str>, + ttl: Option<&str>, + ca_cert_pem: Option, + ca_key_pem: Option +) -> Result<(String, String), RvError> { + let not_before = SystemTime::now(); + let not_after = not_before + parse_duration(ttl.unwrap_or("5d"))?; + let mut subject_name = X509NameBuilder::new()?; + subject_name.append_entry_by_text("C", "CN")?; + subject_name.append_entry_by_text("ST", "ZJ")?; + subject_name.append_entry_by_text("L", "HZ")?; + subject_name.append_entry_by_text("O", "Ant-Group")?; + subject_name.append_entry_by_text("CN", common_name)?; + + let subject = subject_name.build(); + + let mut cert = Certificate { + not_before, + not_after, + subject, + is_ca, + ..Default::default() + }; + + if let Some(dns) = dns_sans { + cert.dns_sans = dns.split(',').map(|s| s.trim().to_string()).collect(); + } + + if let Some(ip) = ip_sans { + cert.ip_sans = ip.split(',').map(|s| s.trim().to_string()).collect(); + } + + if let Some(uri) = uri_sans { + cert.uri_sans = uri.split(',').map(|s| s.trim().to_string()).collect(); + } + + let pkey = PKey::from_rsa(Rsa::generate(2048)?)?; + + let x509 = match (ca_cert_pem, ca_key_pem) { + (Some(cert_pem), Some(key_pem)) => { + let ca_cert = X509::from_pem(cert_pem.as_bytes())?; + let ca_key = PKey::private_key_from_pem(key_pem.as_bytes())?; + cert_to_x509(&cert, client_auth, server_auth, Some(&ca_cert), Some(&ca_key), &pkey)? + }, + _ => cert_to_x509(&cert, client_auth, server_auth, None, None, &pkey)? + }; + + Ok(( + String::from_utf8(x509.to_pem()?)?, + String::from_utf8(pkey.private_key_to_pem_pkcs8()?)?, + )) +} + +pub fn new_test_cert_ext( + is_ca: bool, + client_auth: bool, + server_auth: bool, + common_name: &str, + dns_sans: Option<&str>, + ip_sans: Option<&str>, + uri_sans: Option<&str>, + ttl: Option<&str>, + ca_cert_pem: Option, + ca_key_pem: Option +) -> Result<(String, String), RvError> { + let not_before = SystemTime::now(); + let not_after = not_before + parse_duration(ttl.unwrap_or("5d"))?; + let mut subject_name = X509NameBuilder::new()?; + subject_name.append_entry_by_text("C", "CN")?; + subject_name.append_entry_by_text("ST", "ZJ")?; + subject_name.append_entry_by_text("L", "HZ")?; + subject_name.append_entry_by_text("O", "Ant-Group")?; + subject_name.append_entry_by_text("OU", "engineering")?; + subject_name.append_entry_by_text("CN", common_name)?; + + let subject = subject_name.build(); + + let extensions = vec![X509Extension::new_from_der( + &Asn1Object::from_str("2.1.1.1").unwrap(), + false, + &Asn1OctetString::new_from_bytes(b"A UTF8String Extension").unwrap(), + ).unwrap(), + X509Extension::new_from_der( + &Asn1Object::from_str("2.1.1.2").unwrap(), + false, + &Asn1OctetString::new_from_bytes(b"A UTF8 Extension").unwrap(), + ).unwrap(), + X509Extension::new_from_der( + &Asn1Object::from_str("2.1.1.3").unwrap(), + false, + &Asn1OctetString::new_from_bytes(b"An IA5 Extension").unwrap(), + ).unwrap(), + X509Extension::new_from_der( + &Asn1Object::from_str("2.1.1.4").unwrap(), + false, + &Asn1OctetString::new_from_bytes(b"A Visible Extension").unwrap(), + ).unwrap()]; + + let mut cert = Certificate { + not_before, + not_after, + subject, + is_ca, + extensions, + ..Default::default() + }; + + if !is_ca { + cert.email_sans = vec!["valid@example.com".into()]; + } + + if let Some(dns) = dns_sans { + cert.dns_sans = dns.split(',').map(|s| s.trim().to_string()).collect(); + } + + if let Some(ip) = ip_sans { + cert.ip_sans = ip.split(',').map(|s| s.trim().to_string()).collect(); + } + + if let Some(uri) = uri_sans { + cert.uri_sans = uri.split(',').map(|s| s.trim().to_string()).collect(); + } + + let pkey = PKey::from_rsa(Rsa::generate(2048)?)?; + + let x509 = match (ca_cert_pem, ca_key_pem) { + (Some(cert_pem), Some(key_pem)) => { + let ca_cert = X509::from_pem(cert_pem.as_bytes())?; + let ca_key = PKey::private_key_from_pem(key_pem.as_bytes())?; + cert_to_x509(&cert, client_auth, server_auth, Some(&ca_cert), Some(&ca_key), &pkey)? + }, + _ => cert_to_x509(&cert, client_auth, server_auth, None, None, &pkey)? + }; + + Ok(( + String::from_utf8(x509.to_pem()?)?, + String::from_utf8(pkey.private_key_to_pem_pkcs8()?)?, + )) +} + +pub fn cert_to_x509( + cert: &Certificate, + client_auth: bool, + server_auth: bool, + ca_cert: Option<&X509Ref>, + ca_key: Option<&PKey>, + private_key: &PKey, +) -> Result { + let mut builder = X509::builder()?; + builder.set_version(cert.version)?; + let serial_number = cert.serial_number.to_asn1_integer()?; + builder.set_serial_number(&serial_number)?; + builder.set_subject_name(&cert.subject)?; + if ca_cert.is_some() { + builder.set_issuer_name(ca_cert.unwrap().subject_name())?; + } else { + builder.set_issuer_name(&cert.subject)?; + } + builder.set_pubkey(private_key)?; + + let not_before_dur = cert.not_before.duration_since(UNIX_EPOCH)?; + let not_before = Asn1Time::from_unix(not_before_dur.as_secs() as i64)?; + builder.set_not_before(¬_before)?; + + let not_after_dur = cert.not_after.duration_since(UNIX_EPOCH)?; + let not_after_sec = not_after_dur.as_secs(); + let not_after = Asn1Time::from_unix(not_after_sec as i64)?; + builder.set_not_after(¬_after)?; + + let mut san_ext = SubjectAlternativeName::new(); + for dns in &cert.dns_sans { + san_ext.dns(dns.as_str()); + } + + for email in &cert.email_sans { + san_ext.email(email.as_str()); + } + + for ip in &cert.ip_sans { + san_ext.ip(ip.as_str()); + } + + for uri in &cert.uri_sans { + san_ext.uri(uri.as_str()); + } + + if (cert.dns_sans.len() | cert.email_sans.len() | cert.ip_sans.len() | cert.uri_sans.len()) > 0 { + builder.append_extension(san_ext.build(&builder.x509v3_context(ca_cert, None))?)?; + } + + for ext in &cert.extensions { + builder.append_extension2(ext)?; + } + + if cert.is_ca { + builder.append_extension(BasicConstraints::new().critical().ca().build()?)?; + builder.append_extension(KeyUsage::new().critical().key_cert_sign().crl_sign().build()?)?; + } else { + builder.append_extension(BasicConstraints::new().critical().build()?)?; + builder.append_extension( + KeyUsage::new().critical().non_repudiation().digital_signature().key_encipherment().build()?, + )?; + let mut ext = &mut ExtendedKeyUsage::new(); + if client_auth { + ext = ext.client_auth(); + } + + if server_auth { + ext = ext.server_auth(); + } + //builder.append_extension(ExtendedKeyUsage::new().server_auth().client_auth().build()?)?; + builder.append_extension(ext.build()?)?; + } + + let subject_key_id = SubjectKeyIdentifier::new().build(&builder.x509v3_context(ca_cert, None))?; + builder.append_extension(subject_key_id)?; + + let authority_key_id = + AuthorityKeyIdentifier::new().keyid(true).issuer(false).build(&builder.x509v3_context(ca_cert, None))?; + builder.append_extension(authority_key_id)?; + + if ca_key.is_some() { + builder.sign(ca_key.as_ref().unwrap(), MessageDigest::sha256())?; + } else { + builder.sign(private_key, MessageDigest::sha256())?; + } + + Ok(builder.build()) +} + +pub unsafe fn new_test_crl( + revoked_cert_pem: &str, + ca_cert_pem: &str, + ca_key_pem: &str, +) -> Result { + let revoked_cert = X509::from_pem(revoked_cert_pem.as_bytes())?; + let ca_cert = X509::from_pem(ca_cert_pem.as_bytes())?; + let ca_key = PKey::private_key_from_pem(ca_key_pem.as_bytes())?; + + let crl = openssl_sys::X509_CRL_new(); + if crl.is_null() { + return Err(rv_error_response!("X509_CRL_new failed.")); + } + + if openssl_sys::X509_CRL_set_version(crl, 0) == 0 { + openssl_sys::X509_CRL_free(crl); + return Err(rv_error_response!("X509_CRL_set_version failed.")); + } + + let issuer_name = openssl_sys::X509_get_subject_name(ca_cert.as_ptr()); + if openssl_sys::X509_CRL_set_issuer_name(crl, issuer_name) == 0 { + openssl_sys::X509_CRL_free(crl); + return Err(rv_error_response!("X509_CRL_set_issuer_name failed.")); + } + + let last_update = Asn1Time::days_from_now(0)?; + let next_update = Asn1Time::days_from_now(7)?; + + openssl_sys::X509_CRL_set1_lastUpdate(crl, last_update.as_ptr()); + openssl_sys::X509_CRL_set1_nextUpdate(crl, next_update.as_ptr()); + + let revoked = openssl_sys::X509_REVOKED_new(); + if revoked.is_null() { + openssl_sys::X509_CRL_free(crl); + return Err(rv_error_response!("X509_REVOKED_new failed.")); + } + + let serial_number = openssl_sys::X509_get_serialNumber(revoked_cert.as_ptr()); + openssl_sys::X509_REVOKED_set_serialNumber(revoked, serial_number); + openssl_sys::X509_REVOKED_set_revocationDate(revoked, last_update.as_ptr()); + openssl_sys::X509_CRL_add0_revoked(crl, revoked); + + if openssl_sys::X509_CRL_sign(crl, ca_key.as_ptr(), openssl_sys::EVP_sha256()) == 0 { + openssl_sys::X509_REVOKED_free(revoked); + openssl_sys::X509_CRL_free(crl); + return Err(rv_error_response!("X509_CRL_sign failed.")); + } + + let bio = openssl_sys::BIO_new(openssl_sys::BIO_s_mem()); + openssl_sys::PEM_write_bio_X509_CRL(bio, crl); + + let mut buffer = vec![0u8; 2048]; + let _ = openssl_sys::BIO_read(bio, buffer.as_mut_ptr() as *mut libc::c_void, buffer.len() as c_int); + + openssl_sys::BIO_free_all(bio); + openssl_sys::X509_CRL_free(crl); + + return Ok(String::from_utf8_lossy(&buffer).into()); +} + pub fn test_backend(name: &str) -> Arc { let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); let test_dir = env::temp_dir().join(format!("{}/{}-{}", *TEST_DIR, name, now).as_str()); @@ -89,6 +672,78 @@ pub fn test_rusty_vault_init(name: &str) -> (String, Arc>) { (root_token, c) } +pub fn new_test_http_server(core: Arc>, tls_config: Option) -> Result<(Server, String), RvError> { + let mut http_server = HttpServer::new(move || { + App::new() + .wrap(middleware::Logger::default()) + .app_data(web::Data::new(core.clone())) + .configure(http::init_service) + .default_service(web::to(|| HttpResponse::NotFound())) + }) + .on_connect(http::request_on_connect_handler); + + if let Some(tls) = tls_config { + let cert_file: &Path = Path::new(&tls.cert_path); + let key_file: &Path = Path::new(&tls.key_path); + + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; + builder + .set_private_key_file(key_file, SslFiletype::PEM) + .map_err(|err| format_err!("unable to read proxy key {} - {}", key_file.display(), err))?; + builder + .set_certificate_chain_file(cert_file) + .map_err(|err| format_err!("unable to read proxy cert {} - {}", cert_file.display(), err))?; + builder.check_private_key()?; + + builder.set_min_proto_version(Some(SslVersion::TLS1_2))?; + builder.set_max_proto_version(Some(SslVersion::TLS1_3))?; + + builder.set_cipher_list("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:HIGH:!PSK:!SRP:!3DES")?; + + builder.set_verify_callback(SslVerifyMode::PEER, |_, _| true); + + http_server = http_server.bind_openssl("127.0.0.1:0", builder)?; + } else { + http_server = http_server.bind("127.0.0.1:0")?; + } + + let addr_info = http_server.addrs().first().unwrap().to_string(); + + println!("HTTP Server is running at {}", addr_info); + + Ok((http_server.run(), addr_info)) +} + +pub fn start_test_http_server(server: Server, barrier: Arc, stop_rx: oneshot::Receiver<()>) -> thread::JoinHandle<()> { + let server_thread = thread::spawn(move || { + let sys = actix_web::rt::System::new(); + + let server_future = async { + server.await.unwrap(); + }; + + let stop_future = async { + stop_rx.await.ok(); + }; + + barrier.wait(); + + let _ = sys.block_on(async { + tokio::select! { + _ = server_future => {}, + _ = stop_future => { + actix_rt::System::current().stop(); + } + } + }); + + let _ = sys.run().unwrap(); + println!("HTTP Server has stopped."); + }); + + server_thread +} + pub fn test_list_api(core: &Core, token: &str, path: &str, is_ok: bool) -> Result, RvError> { let mut req = Request::new(path); req.operation = Operation::List; @@ -115,7 +770,7 @@ pub fn test_write_api( path: &str, is_ok: bool, data: Option>, - ) -> Result, RvError> { +) -> Result, RvError> { let mut req = Request::new(path); req.operation = Operation::Write; req.client_token = token.to_string(); @@ -133,7 +788,7 @@ pub fn test_delete_api( path: &str, is_ok: bool, data: Option>, - ) -> Result, RvError> { +) -> Result, RvError> { let mut req = Request::new(path); req.operation = Operation::Delete; req.client_token = token.to_string(); @@ -167,4 +822,3 @@ pub fn test_mount_auth_api(core: &Core, token: &str, atype: &str, path: &str) { let resp = test_write_api(core, token, format!("sys/auth/{}", path).as_str(), true, Some(auth_data)); assert!(resp.is_ok()); } - diff --git a/src/utils/cert.rs b/src/utils/cert.rs index 3fa900e..ef2cf2c 100644 --- a/src/utils/cert.rs +++ b/src/utils/cert.rs @@ -1,5 +1,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; +use better_default::Default; use foreign_types::ForeignType; use lazy_static::lazy_static; use libc::c_int; @@ -19,9 +20,12 @@ use openssl::{ X509Builder, X509Extension, X509Name, X509NameBuilder, X509Ref, X509, }, }; +use openssl_sys::{ + X509_get_extended_key_usage, X509_get_extension_flags, EXFLAG_XKUSAGE, + stack_st_X509, X509_STORE_CTX, +}; use serde::{ser::SerializeTuple, Deserialize, Deserializer, Serialize, Serializer}; use serde_bytes::ByteBuf; -use better_default::Default; use crate::errors::RvError; @@ -32,6 +36,7 @@ lazy_static! { extern "C" { pub fn X509_check_ca(x509: *mut openssl_sys::X509) -> c_int; + pub fn X509_STORE_CTX_set0_trusted_stack(ctx: *mut X509_STORE_CTX, chain: *mut stack_st_X509); } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -100,7 +105,6 @@ fn deserialize_pkey<'de, D>(deserializer: D) -> Result, { - //let pem_bytes: &[u8] = Deserialize::deserialize(deserializer)?; let pem_bytes: &[u8] = &ByteBuf::deserialize(deserializer)?; PKey::private_key_from_pem(pem_bytes).map_err(serde::de::Error::custom) } @@ -115,6 +119,17 @@ pub fn generate_serial_number() -> BigNum { sn } +pub fn has_x509_ext_key_usage(x509: &X509) -> bool { + unsafe { X509_get_extension_flags(x509.as_ptr()) & EXFLAG_XKUSAGE != 0 } +} + +pub fn has_x509_ext_key_usage_flag(x509: &X509, flag: u32) -> bool { + unsafe { + (X509_get_extension_flags(x509.as_ptr()) & EXFLAG_XKUSAGE != 0) + && (X509_get_extended_key_usage(x509.as_ptr()) & flag) != 0 + } +} + impl CertBundle { pub fn new() -> Self { CertBundle::default() @@ -168,7 +183,7 @@ impl CertBundle { #[derive(Default)] pub struct Certificate { - #[default(3)] + #[default(0x2)] pub version: i32, #[default(generate_serial_number())] pub serial_number: BigNum, @@ -216,8 +231,7 @@ impl Certificate { builder.set_pubkey(private_key)?; let not_before_dur = self.not_before.duration_since(UNIX_EPOCH)?; - let not_before_sec = not_before_dur.as_secs() - 30; - let not_before = Asn1Time::from_unix(not_before_sec as i64)?; + let not_before = Asn1Time::from_unix(not_before_dur.as_secs() as i64)?; builder.set_not_before(¬_before)?; let not_after_dur = self.not_after.duration_since(UNIX_EPOCH)?; @@ -242,7 +256,9 @@ impl Certificate { san_ext.uri(uri.as_str()); } - builder.append_extension(san_ext.build(&builder.x509v3_context(ca_cert, None))?)?; + if (self.dns_sans.len() | self.email_sans.len() | self.ip_sans.len() | self.uri_sans.len()) > 0 { + builder.append_extension(san_ext.build(&builder.x509v3_context(ca_cert, None))?)?; + } for ext in &self.extensions { builder.append_extension2(ext)?; diff --git a/src/utils/key.rs b/src/utils/key.rs index cee7a05..9ecbe66 100644 --- a/src/utils/key.rs +++ b/src/utils/key.rs @@ -1,3 +1,4 @@ +use better_default::Default; use openssl::{ ec::{EcGroup, EcKey}, hash::MessageDigest, @@ -9,7 +10,6 @@ use openssl::{ symm::{decrypt, decrypt_aead, encrypt, encrypt_aead, Cipher}, }; use serde::{Deserialize, Serialize}; -use better_default::Default; use crate::{errors::RvError, utils::generate_uuid}; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 45bf24f..15dce2c 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -17,13 +17,13 @@ pub mod crypto; pub mod ip_sock_addr; pub mod key; pub mod locks; +pub mod ocsp; pub mod policy; pub mod salt; pub mod sock_addr; pub mod string; pub mod token_util; pub mod unix_sock_addr; -pub mod ocsp; pub fn generate_uuid() -> String { let mut buf = [0u8; 16]; diff --git a/src/utils/ocsp.rs b/src/utils/ocsp.rs index ab9d4d3..0c55ad3 100644 --- a/src/utils/ocsp.rs +++ b/src/utils/ocsp.rs @@ -1,8 +1,5 @@ use better_default::Default; - -use openssl::{ - x509::X509, -}; +use openssl::x509::X509; #[repr(u32)] #[derive(Debug)] diff --git a/src/utils/salt.rs b/src/utils/salt.rs index 770f84d..d67b50c 100644 --- a/src/utils/salt.rs +++ b/src/utils/salt.rs @@ -1,8 +1,8 @@ //! This module is a Rust replica of //! -use derivative::Derivative; use better_default::Default; +use derivative::Derivative; use openssl::{ hash::{hash, MessageDigest}, nid::Nid, diff --git a/src/utils/sock_addr.rs b/src/utils/sock_addr.rs index 779c5ab..a5b9dcb 100644 --- a/src/utils/sock_addr.rs +++ b/src/utils/sock_addr.rs @@ -70,6 +70,12 @@ impl fmt::Display for SockAddrMarshaler { } } +impl PartialEq for SockAddrMarshaler { + fn eq(&self, other: &Self) -> bool { + self.sock_addr.equal(&*other.sock_addr) + } +} + impl Serialize for SockAddrMarshaler { fn serialize(&self, serializer: S) -> Result where diff --git a/src/utils/token_util.rs b/src/utils/token_util.rs index 40be675..78d2599 100644 --- a/src/utils/token_util.rs +++ b/src/utils/token_util.rs @@ -10,20 +10,42 @@ use crate::{ utils::{deserialize_duration, serialize_duration, sock_addr::SockAddrMarshaler}, }; +/* +const DEFAULT_LEASE_TTL: Duration = Duration::from_secs(365 * 24 * 60 * 60 as u64); +const MAX_LEASE_TTL: Duration = Duration::from_secs(365 * 24 * 60 * 60 as u64); +*/ + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenParams { pub token_type: String, + + // The TTL to user for the token #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] pub token_ttl: Duration, + + // The max TTL to use for the token #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] pub token_max_ttl: Duration, + + // If set, the token entry will have an explicit maximum TTL set, rather than deferring to role/mount values #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] pub token_explicit_max_ttl: Duration, + + // If non-zero, tokens created using this role will be able to be renewed forever, + // but will have a fixed renewal period of this value #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] pub token_period: Duration, + + // If set, core will not automatically add default to the policy list pub token_no_default_policy: bool, + + // The maximum number of times a token issued from this role may be used. pub token_num_uses: u64, + + // The policies to set pub token_policies: Vec, + + // The set of CIDRs that tokens generated using this role will be bound to pub token_bound_cidrs: Vec, }