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, }