diff --git a/Cargo.toml b/Cargo.toml index afd2f02ea311cda3d7febb4b588ec161bb853e21..70f21ca128ddb3116c0865aee1bde60c892b69d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ clap = { version = "4.0.22", features = ["derive", "env"] } config = "0.13.3" lazy_static = "1.4.0" actix-web = { version = "4.3.0", features = ["openssl"]} +actix-web-lab = "0.19.1" tonic = {version = "0.8.2", features = ["tls", "tls-roots", "transport", "channel"]} prost = "0.11.0" signal-hook = "0.3.14" @@ -75,6 +76,8 @@ utoipa = { version = "3", features = ["actix_extras"] } utoipa-swagger-ui = { version ="3.1.3", features = ["actix-web"]} efi_signer = "0.2.4" regex = "1" +csrf= "0.4.1" +data-encoding= "2.4.0" [build-dependencies] tonic-build = "0.8.4" diff --git a/src/application/datakey.rs b/src/application/datakey.rs index c86800f1f15d5c142033c75e7cfffa50fda3ec27..37a730b92ae47cfecd5445a1d05077d25dcfeb94 100644 --- a/src/application/datakey.rs +++ b/src/application/datakey.rs @@ -155,8 +155,8 @@ impl DBKeyService if data.key_type == X509EE && parent_key.key_type != X509ICA { return Err(Error::ActionsNotAllowedError("only ICA key is allowed for creating End Entity Key".to_string())); } - if data.key_type == X509CA { - return Err(Error::ActionsNotAllowedError("CA key is not allowed to specify parent key".to_string())); + if data.key_type == X509CA || data.key_type == OpenPGP { + return Err(Error::ActionsNotAllowedError("CA key or openPGP is not allowed to specify parent key".to_string())); } Ok(()) } diff --git a/src/application/user.rs b/src/application/user.rs index 139ee61bc49373800db8f4a5c5715055649370ce..f04e6538eb8370e4e595f1e4b153bc008426fee1 100644 --- a/src/application/user.rs +++ b/src/application/user.rs @@ -27,7 +27,7 @@ use tokio::sync::RwLock as AsyncRwLock; use chrono::Utc; use serde::{Deserialize}; use config::Config; -use reqwest::{header, Client}; +use reqwest::{header, Client, StatusCode}; use crate::presentation::handler::control::model::user::dto::UserIdentity; use openidconnect::{ Scope, @@ -143,13 +143,18 @@ impl DBUserService async fn get_access_token(&self, code: &str) -> Result { match Client::builder().build() { Ok(client) => { - let token: AccessToken = client.post(&self.oidc_config.token_url).query(&[ + let response= client.post(&self.oidc_config.token_url).query(&[ ("client_id", self.oidc_config.client_id.as_str()), ("client_secret", self.oidc_config.client_secret.as_str()), ("code", code), ("redirect_uri", self.oidc_config.redirect_uri.as_str()), - ("grant_type", "authorization_code")]).send().await?.json().await?; - Ok(token) + ("grant_type", "authorization_code")]).send().await?; + if response.status() != StatusCode::OK { + Err(Error::AuthError(format!("failed to get access token {}", response.text().await?))) + } else { + let resp: AccessToken = response.json().await?; + Ok(resp) + } } Err(err) => { Err(Error::AuthError(err.to_string())) diff --git a/src/control_admin_entrypoint.rs b/src/control_admin_entrypoint.rs index 4ab080e7232b8f92c54fdab9cfa1d74c9c58480c..9f98efb0f545c5d8618778e8b136beea7b551ffa 100644 --- a/src/control_admin_entrypoint.rs +++ b/src/control_admin_entrypoint.rs @@ -188,7 +188,7 @@ async fn main() -> Result<()> { } key.validate()?; - let keys = control_server.create_keys(&mut DataKey::create_from(key, UserIdentity::from(user))?).await?; + let keys = control_server.create_keys(&mut DataKey::create_from(key, UserIdentity::from_user(user))?).await?; info!("[Result]: Keys {} type {} has been successfully generated", &keys.name, &generate_keys.key_type) } None => {} diff --git a/src/control_server_entrypoint.rs b/src/control_server_entrypoint.rs index 82a77f5c60765059fad0d5f7afe5f94f92b788a4..dc2f89b04976748469d91c82dc59b3d8fbc1f85b 100644 --- a/src/control_server_entrypoint.rs +++ b/src/control_server_entrypoint.rs @@ -74,7 +74,8 @@ lazy_static! { let path = app.config.unwrap_or(format!("{}/{}", env::current_dir().expect("current dir not found").display(), "config/server.toml")); let server_config = util::config::ServerConfig::new(path); - server_config.watch(CANCEL_TOKEN.clone()).expect("failed to watch configure file"); + //TODO: Enable watch configure file will lead to 100 percent cpu consumption, fix it later + //server_config.watch(CANCEL_TOKEN.clone()).expect("failed to watch configure file"); server_config.config }; } diff --git a/src/data_server_entrypoint.rs b/src/data_server_entrypoint.rs index fa1638f6ea3af917c8f0b47c44da866f2e01c308..a00e8a25af70da4cb995640f9351ea7bed42a8aa 100644 --- a/src/data_server_entrypoint.rs +++ b/src/data_server_entrypoint.rs @@ -76,7 +76,8 @@ lazy_static! { let path = app.config.unwrap_or(format!("{}/{}", env::current_dir().expect("current dir not found").display(), "config/server.toml")); let server_config = util::config::ServerConfig::new(path); - server_config.watch(CANCEL_TOKEN.clone()).expect("failed to watch configure file"); + //TODO: Enable watch configure file will lead to 100 percent cpu consumption, fix it later + //server_config.watch(CANCEL_TOKEN.clone()).expect("failed to watch configure file"); server_config.config }; } diff --git a/src/presentation/handler/control/model/user/dto.rs b/src/presentation/handler/control/model/user/dto.rs index 738db01bf72489f84a7cff5891332b1598dd526d..a1f366c858670c1e2160ed6f36383e57455294bf 100644 --- a/src/presentation/handler/control/model/user/dto.rs +++ b/src/presentation/handler/control/model/user/dto.rs @@ -1,21 +1,122 @@ -use actix_web::{Result, HttpRequest, FromRequest, dev::Payload}; -use crate::util::error::{Error}; - +use actix_web::{Result, HttpRequest, FromRequest, dev::Payload, dev::ServiceRequest, body::MessageBody,dev::ServiceResponse}; +use crate::util::error::{Error, Result as SignatrustResult}; +use std::convert::TryInto; use actix_identity::Identity; +use actix_web_lab::middleware::Next; use actix_web::web; use std::pin::Pin; use futures::Future; use serde::{Deserialize, Serialize}; use std::convert::From; +use actix_web::http::header::HeaderName; use crate::application::user::UserService; use crate::domain::user::entity::User; use utoipa::{IntoParams, ToSchema}; use validator::Validate; +use csrf::{AesGcmCsrfProtection, CsrfProtection}; +use data_encoding::BASE64; +use reqwest::header::HeaderValue; +use reqwest::StatusCode; +use secstr::SecVec; +use crate::util::error::Error::GeneratingKeyError; +use crate::util::key::generate_csrf_parent_token; + +pub const CSRF_HEADER_NAME: &str = "Xsrf-Token"; +pub const AUTH_HEADER_NAME: &str = "Authorization"; +pub const SET_COOKIE_HEADER: &str = "set-cookie"; + #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct UserIdentity { pub email: String, pub id: i32, + //these two only exist when calling from OIDC login + pub csrf_generation_token: Option>, + pub csrf_token: Option +} + +impl UserIdentity { + pub fn from_user(id: User) -> Self { + UserIdentity { + id: id.id, + email: id.email, + csrf_token: None, + csrf_generation_token: None + } + } + + pub fn from_user_with_csrf_token(id: User, protect_key: [u8; 32]) -> SignatrustResult { + let protect = AesGcmCsrfProtection::from_key(protect_key); + let random_token = generate_csrf_parent_token(); + let random_token_array = random_token.clone().try_into()?; + //we don't use cookie here + let token = protect.generate_token(&random_token_array)?; + Ok(UserIdentity { + id: id.id, + email: id.email, + csrf_generation_token: Some(random_token.to_vec()), + csrf_token: Some(token.b64_string()) + }) + } + + pub fn generate_new_csrf_cookie(&self, protect_key: [u8; 32], ttl_seconds: i64) -> SignatrustResult { + if self.csrf_generation_token.is_none() || self.csrf_token.is_none() { + return Err(GeneratingKeyError("csrf token is empty, cannot generate new csrf cookie".to_string())); + } + let protect = AesGcmCsrfProtection::from_key(protect_key); + let generation_token: [u8; 64] = self.csrf_generation_token.clone().unwrap().try_into()?; + let cookie = protect.generate_cookie( + &generation_token, ttl_seconds)?; + Ok(cookie.b64_string()) + } + + pub fn csrf_cookie_valid(&self, protect_key: [u8; 32], value: &str) -> SignatrustResult { + let protect = AesGcmCsrfProtection::from_key(protect_key); + Ok(protect.verify_token_pair( + &protect.parse_token( + &BASE64.decode(self.csrf_token.clone().unwrap().as_bytes())?)?, + &protect.parse_cookie( + &BASE64.decode(value.as_bytes())?)?)) + } + + pub async fn append_csrf_cookie(req: ServiceRequest, next: Next) -> core::result::Result, actix_web::error::Error> { + let mut response = next.call(req).await?; + if let Ok(identity) = Identity::from_request(response.request(), &mut Payload::None).into_inner() { + if let Ok(user_json) = identity.id() { + if let Ok(user) = serde_json::from_str::(&user_json) { + if response.status() == StatusCode::UNAUTHORIZED { + //only append csrf token in authorized response + return Ok(response); + } + //generate csrf cookie based on user token + if let Some(protect_key) = response.request().app_data::>>() { + if let Ok(protect_key_array) = protect_key.clone().unsecure().try_into() { + if let Ok(csrf_token) = user.generate_new_csrf_cookie(protect_key_array, 600) { + let http_header = response.headers_mut(); + http_header.insert( + HeaderName::from_static(SET_COOKIE_HEADER), + HeaderValue::from_str(&format!("{}={}; Secure; Path=/; Max-Age=600", CSRF_HEADER_NAME, csrf_token)).unwrap(), + ); + } else { + warn!("failed to generate csrf token in middleware"); + } + } + } + } + } + } + + Ok(response) + } +} + +impl From for User { + fn from(id: UserIdentity) -> Self { + User { + id: id.id, + email: id.email + } + } } impl FromRequest for UserIdentity { @@ -24,7 +125,7 @@ impl FromRequest for UserIdentity { fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future { let mut login: Option = None; - //fetch from session + //fetch id from session if let Ok(identity) = Identity::from_request(&req.clone(), pl).into_inner() { if let Ok(user_json) = identity.id() { if let Ok(user) = serde_json::from_str(&user_json) { @@ -35,13 +136,15 @@ impl FromRequest for UserIdentity { let req = req.clone(); Box::pin(async move { match login { - //fetch valid token + // if API is invoked with API token, + // we need to fetch user identity from database + // and check whether the token is expired None => { - if let Some(value) = req.headers().get("Authorization") { + if let Some(value) = req.headers().get(AUTH_HEADER_NAME) { if let Some(user_service) = req.app_data::>() { if let Ok(token) = user_service.get_ref().get_valid_token(value.to_str().unwrap()).await { if let Ok(user) = user_service.get_ref().get_user_by_id(token.user_id).await { - return Ok(UserIdentity::from(user)); + return Ok(UserIdentity::from_user(user)); } } else { warn!("unable to find token record"); @@ -52,34 +155,33 @@ impl FromRequest for UserIdentity { } Err(Error::UnauthorizedError) } + // or we have to check both the token and csrf value. Some(user) => { - Ok(user) + if let Some(protect_key) = req.app_data::>>() { + if let Some(header) = req.headers().get(CSRF_HEADER_NAME) { + if let Ok(protect_key_array) = protect_key.clone().unsecure().try_into() { + if let Ok(true) = user.csrf_cookie_valid(protect_key_array, header.to_str().unwrap()) { + return Ok(user) + } else { + warn!("csrf header is invalid"); + } + } + } else { + warn!("unable to find csrf cookie"); + } + } else { + warn!("unable to find csrf protect key"); + } + Err(Error::UnauthorizedError) } } }) } } -impl From for User { - fn from(id: UserIdentity) -> Self { - User { - id: id.id, - email: id.email - } - } -} - #[derive(Deserialize, IntoParams, Validate, ToSchema)] pub struct Code { #[validate(length(min = 1))] pub code: String, } -impl From for UserIdentity { - fn from(id: User) -> Self { - UserIdentity { - id: id.id, - email: id.email, - } - } -} \ No newline at end of file diff --git a/src/presentation/handler/control/user_handler.rs b/src/presentation/handler/control/user_handler.rs index 1da32cf4f4356f3db67634bddabfd389750320ef..8ca7d6363761336150f28f5b2435f673c28133e1 100644 --- a/src/presentation/handler/control/user_handler.rs +++ b/src/presentation/handler/control/user_handler.rs @@ -18,11 +18,13 @@ use actix_web::{HttpResponse, Responder, Result, web, Scope, HttpRequest, HttpMe use crate::util::error::Error; use super::model::user::dto::UserIdentity; use actix_identity::Identity; +use actix_web::cookie::Cookie; +use secstr::SecVec; use validator::Validate; use crate::application::user::UserService; use crate::presentation::handler::control::model::token::dto::{CreateTokenDTO, TokenDTO}; -use crate::presentation::handler::control::model::user::dto::Code; +use crate::presentation::handler::control::model::user::dto::{Code, CSRF_HEADER_NAME}; /// Start the login OIDC login process /// @@ -103,12 +105,20 @@ async fn logout(id: Identity) -> Result { (status = 500, description = "Server internal error", body = ErrorMessage) ) )] -async fn callback(req: HttpRequest, user_service: web::Data, code: web::Query) -> Result { +async fn callback(req: HttpRequest, user_service: web::Data, code: web::Query, protect_key: web::Data>) -> Result { code.validate()?; - let user_entity:UserIdentity = UserIdentity::from(user_service.into_inner().validate_user(&code.code).await?); + //generate csrf token and cookie + let protect_key_array:[u8; 32] = protect_key.unsecure().try_into()?; + let user_entity = UserIdentity::from_user_with_csrf_token(user_service.validate_user(&code.code).await?, protect_key_array)?; match Identity::login(&req.extensions(), serde_json::to_string(&user_entity)?) { Ok(_) => { - Ok(HttpResponse::Found().insert_header(("Location", "/")).finish()) + let cookie = Cookie::build(CSRF_HEADER_NAME, user_entity.generate_new_csrf_cookie(protect_key_array, 3600)?) + .path("/") + .secure(true) + .same_site(actix_web::cookie::SameSite::Strict) + .expires(time::OffsetDateTime::now_utc() + time::Duration::seconds(3600)) + .finish(); + Ok(HttpResponse::Found().cookie(cookie).insert_header(("Location", "/")).finish()) } Err(err) => { Err(Error::AuthError(format!("failed to get oidc token {}", err))) diff --git a/src/presentation/server/control_server.rs b/src/presentation/server/control_server.rs index 7a245c9f4603e9a594675896cd32abc1ba2ff712..1d3e5b8057c8ac53d48240ea678383787c875882 100644 --- a/src/presentation/server/control_server.rs +++ b/src/presentation/server/control_server.rs @@ -34,6 +34,8 @@ use crate::infra::database::model::datakey::repository as datakeyRepository; use crate::infra::database::pool::{create_pool, get_db_pool}; use crate::presentation::handler::control::*; use actix_web::{dev::ServiceRequest}; +use actix_web::cookie::SameSite; +use secstr::SecVec; use tokio_util::sync::CancellationToken; use crate::util::error::Result; @@ -46,7 +48,8 @@ use crate::domain::datakey::entity::DataKey; use crate::domain::token::entity::Token; use crate::domain::user::entity::User; use crate::presentation::handler::control::model::token::dto::{CreateTokenDTO}; -use crate::presentation::handler::control::model::user::dto::UserIdentity; +use crate::presentation::handler::control::model::user::dto::{UserIdentity}; +use crate::util::key::truncate_string_to_protect_key; pub struct ControlServer { server_config: Arc>, @@ -190,9 +193,15 @@ impl ControlServer { ); let openapi = ControlApiDoc::openapi(); + let csrf_protect_key = web::Data::new(SecVec::new(truncate_string_to_protect_key(&key).to_vec())); let http_server = HttpServer::new(move || { App::new() + //NOTE: csrf protect,following the suggestion from https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html + //there is no need to update csrf cookie for every request + //in the case of signed double submit cookie ,disable updating csrf token in middleware automatically + //now and open it if we have to. + //.wrap(from_fn(UserIdentity::append_csrf_cookie)) .wrap(middleware::Logger::default()) .wrap(IdentityMiddleware::default()) //rate limiter handler @@ -203,12 +212,13 @@ impl ControlServer { store.clone(), Key::from(key.as_bytes())) .session_lifecycle(PersistentSession::default().session_ttl(timeDuration::hours(1))) .cookie_name("Signatrust".to_owned()) - .cookie_secure(false) + .cookie_secure(true) .cookie_domain(None) + .cookie_same_site(SameSite::Strict) .cookie_path("/".to_owned()) .build(), ) - // enable logger + .app_data(csrf_protect_key.clone()) .app_data(key_service.clone()) .app_data(user_service.clone()) .app_data(limiter.clone()) @@ -249,7 +259,7 @@ impl ControlServer { pub async fn create_user_token(&self, user: User) -> Result { let user = self.user_service.save(user).await?; self.user_service.generate_token( - &UserIdentity::from(user.clone()), + &UserIdentity::from_user(user.clone()), CreateTokenDTO::new("default admin token".to_owned())).await } diff --git a/src/util/error.rs b/src/util/error.rs index 69d8d4ac8adec9615316433dce6cdbee0d3c44db..270b76c44361d5edc3924cfd3ac0eec5e9ea051e 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -14,6 +14,8 @@ * */ +use std::array::TryFromSliceError; +use std::convert::Infallible; use config::ConfigError; use pgp::composed::key::SecretKeyParamsBuilderError; use pgp::errors::Error as PGPError; @@ -40,6 +42,7 @@ use openidconnect::url::ParseError as OIDCParseError; use openidconnect::ConfigurationError; use openidconnect::UserInfoError; use anyhow::Error as AnyhowError; +use csrf::CsrfError; use utoipa::{ToSchema}; use efi_signer::error::Error as EFIError; @@ -96,6 +99,8 @@ pub enum Error { UnprivilegedError, #[error("operation disallowed: {0}")] ActionsNotAllowedError(String), + #[error("framework error: {0}")] + FrameworkError(String), //client error #[error("file extension {0} not supported for file {1}")] @@ -363,3 +368,30 @@ impl From for Error { } } +impl From for Error { + fn from(error: CsrfError) -> Self { + Error::FrameworkError(error.to_string()) + } +} + +impl From for Error { + fn from(error: actix_web::Error) -> Self { Error::FrameworkError(error.to_string()) } +} + +impl From for Error { + fn from(error: data_encoding::DecodeError) -> Self { Error::FrameworkError(error.to_string()) } +} + +impl From for Error { + fn from(error: Infallible) -> Self { Error::FrameworkError(error.to_string()) } +} + +impl From for Error { + fn from(error: TryFromSliceError) -> Self { Error::FrameworkError(error.to_string()) } +} + +impl From> for Error { + fn from(error: Vec) -> Self { Error::KeyParseError(format!("original vec {:?}", error)) } +} + + diff --git a/src/util/key.rs b/src/util/key.rs index 41314c062793a508f950be40bb34484195392cfa..4ccb9c891cbaffe91459da52dc91a7436a0257b0 100644 --- a/src/util/key.rs +++ b/src/util/key.rs @@ -36,6 +36,17 @@ pub fn generate_api_token() -> String { thread_rng().sample_iter(&Alphanumeric).take(40).map(char::from).collect() } +pub fn generate_csrf_parent_token() -> Vec { + let number: Vec = (0..64).map(|_| thread_rng().gen::()).collect(); + number +} +pub fn truncate_string_to_protect_key(s: &str) -> [u8; 32] { + let truncated = &s.as_bytes()[..32].to_owned(); + let mut result = [0u8; 32]; + result[..truncated.len()].copy_from_slice(truncated); + result +} + pub fn get_token_hash(real_token: &str) -> String { let mut hasher = Sha256::default(); hasher.update(real_token);