Bug: Support to inject TLS certificate inside Hyperswith App Server




diff --git a/Cargo.lock b/Cargo.lock
index 9b08fcbd57b..af7b0048355 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -43,6 +43,7 @@ dependencies = [
  "actix-codec",
  "actix-rt",
  "actix-service",
+ "actix-tls",
  "actix-utils",
  "ahash 0.8.11",
  "base64 0.21.7",
@@ -187,8 +188,10 @@ dependencies = [
  "http 1.1.0",
  "impl-more",
  "pin-project-lite",
+ "rustls-pki-types",
  "tokio 1.37.0",
  "tokio-rustls 0.23.4",
+ "tokio-rustls 0.25.0",
  "tokio-util",
  "tracing",
  "webpki-roots 0.22.6",
@@ -217,6 +220,7 @@ dependencies = [
  "actix-rt",
  "actix-server",
  "actix-service",
+ "actix-tls",
  "actix-utils",
  "actix-web-codegen",
  "ahash 0.8.11",
@@ -5798,7 +5802,7 @@ dependencies = [
  "percent-encoding",
  "pin-project-lite",
  "rustls 0.21.10",
- "rustls-pemfile",
+ "rustls-pemfile 1.0.4",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -5988,6 +5992,8 @@ dependencies = [
  "roxmltree",
  "rust_decimal",
  "rustc-hash",
+ "rustls 0.22.4",
+ "rustls-pemfile 2.1.2",
  "scheduler",
  "serde",
  "serde_json",
@@ -6217,10 +6223,24 @@ checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
 dependencies = [
  "log",
  "ring 0.17.8",
- "rustls-webpki",
+ "rustls-webpki 0.101.7",
  "sct",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
+dependencies = [
+ "log",
+ "ring 0.17.8",
+ "rustls-pki-types",
+ "rustls-webpki 0.102.3",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "rustls-native-certs"
 version = "0.6.3"
@@ -6228,7 +6248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
 dependencies = [
  "openssl-probe",
- "rustls-pemfile",
+ "rustls-pemfile 1.0.4",
  "schannel",
  "security-framework",
 ]
@@ -6242,6 +6262,22 @@ dependencies = [
  "base64 0.21.7",
 ]
 
+[[package]]
+name = "rustls-pemfile"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
+dependencies = [
+ "base64 0.22.0",
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
+
 [[package]]
 name = "rustls-webpki"
 version = "0.101.7"
@@ -6252,6 +6288,17 @@ dependencies = [
  "untrusted 0.9.0",
 ]
 
+[[package]]
+name = "rustls-webpki"
+version = "0.102.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf"
+dependencies = [
+ "ring 0.17.8",
+ "rustls-pki-types",
+ "untrusted 0.9.0",
+]
+
 [[package]]
 name = "rustversion"
 version = "1.0.14"
@@ -7623,6 +7670,17 @@ dependencies = [
  "tokio 1.37.0",
 ]
 
+[[package]]
+name = "tokio-rustls"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
+dependencies = [
+ "rustls 0.22.4",
+ "rustls-pki-types",
+ "tokio 1.37.0",
+]
+
 [[package]]
 name = "tokio-stream"
 version = "0.1.15"
diff --git a/config/config.example.toml b/config/config.example.toml
index 10f8ba7a5e7..1c3d9ebd98e 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -11,6 +11,15 @@ host = "127.0.0.1"
 shutdown_timeout = 30
 # HTTP Request body limit. Defaults to 32kB
 request_body_limit = 32_768
+
+# HTTPS Server Configuration
+# Self-signed Private Key and Certificate can be generated with mkcert for local development
+[server.tls]
+port = 8081
+host = "127.0.0.1"
+private_key = "/path/to/private_key.pem"
+certificate = "/path/to/certificate.pem"
+
 # Proxy server configuration for connecting to payment gateways.
 # Don't define the fields if a Proxy isn't needed. Empty strings will cause failure.
 [proxy]
diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml
index 68df3d28e24..27567ec6cc7 100644
--- a/config/deployments/env_specific.toml
+++ b/config/deployments/env_specific.toml
@@ -265,6 +265,14 @@ shutdown_timeout = 30
 # HTTP Request body limit. Defaults to 32kB
 request_body_limit = 32_768
 
+# HTTPS Server Configuration
+# Self-signed Private Key and Certificate can be generated with mkcert for local development
+[server.tls]
+port = 8081
+host = "127.0.0.1"
+private_key = "/path/to/private_key.pem"
+certificate = "/path/to/certificate.pem"
+
 [secrets_management]
 secrets_manager = "aws_kms" # Secrets manager client to be used
 
diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml
index dd14c8d8960..e4bfb4a454d 100644
--- a/crates/router/Cargo.toml
+++ b/crates/router/Cargo.toml
@@ -9,7 +9,8 @@ readme = "README.md"
 license.workspace = true
 
 [features]
-default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "payout_retry", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"]
+default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "payout_retry", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm", "tls"]
+tls = ["actix-web/rustls-0_22"]
 email = ["external_services/email", "scheduler/email", "olap"]
 frm = ["api_models/frm", "hyperswitch_domain_models/frm"]
 stripe = ["dep:serde_qs"]
@@ -77,6 +78,8 @@ ring = "0.17.8"
 roxmltree = "0.19.0"
 rust_decimal = { version = "1.35.0", features = ["serde-with-float", "serde-with-str"] }
 rustc-hash = "1.1.0"
+rustls = "0.22"
+rustls-pemfile = "2"
 serde = { version = "1.0.197", features = ["derive"] }
 serde_json = "1.0.115"
 serde_path_to_error = "0.1.16"
diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs
index 35bfbc88f69..80ac62a5df3 100644
--- a/crates/router/src/configs/defaults.rs
+++ b/crates/router/src/configs/defaults.rs
@@ -12,6 +12,8 @@ impl Default for super::settings::Server {
             host: "localhost".into(),
             request_body_limit: 16 * 1024, // POST request body is limited to 16KiB
             shutdown_timeout: 30,
+            #[cfg(feature = "tls")]
+            tls: None,
         }
     }
 }
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index 0569c4dbed6..66df317d7db 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -537,6 +537,8 @@ pub struct Server {
     pub host: String,
     pub request_body_limit: usize,
     pub shutdown_timeout: u64,
+    #[cfg(feature = "tls")]
+    pub tls: Option<ServerTls>,
 }
 
 #[derive(Debug, Deserialize, Clone)]
@@ -816,6 +818,19 @@ pub struct PayPalOnboarding {
     pub enabled: bool,
 }
 
+#[cfg(feature = "tls")]
+#[derive(Debug, Deserialize, Clone)]
+pub struct ServerTls {
+    /// Port to host the TLS secure server on
+    pub port: u16,
+    /// Use a different host (optional) (defaults to the host provided in [`Server`] config)
+    pub host: Option<String>,
+    /// private key file path associated with TLS (path to the private key file (`pem` format))
+    pub private_key: PathBuf,
+    /// certificate file associated with TLS (path to the certificate file (`pem` format))
+    pub certificate: PathBuf,
+}
+
 fn deserialize_hashset_inner<T>(value: impl AsRef<str>) -> Result<HashSet<T>, String>
 where
     T: Eq + std::str::FromStr + std::hash::Hash,
diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs
index f0d332514d1..b653c31ec63 100644
--- a/crates/router/src/lib.rs
+++ b/crates/router/src/lib.rs
@@ -200,11 +200,65 @@ pub async fn start_server(conf: settings::Settings<SecuredSecret>) -> Applicatio
     );
     let state = Box::pin(AppState::new(conf, tx, api_client)).await;
     let request_body_limit = server.request_body_limit;
-    let server = actix_web::HttpServer::new(move || mk_app(state.clone(), request_body_limit))
-        .bind((server.host.as_str(), server.port))?
-        .workers(server.workers)
-        .shutdown_timeout(server.shutdown_timeout)
-        .run();
+
+    let server_builder =
+        actix_web::HttpServer::new(move || mk_app(state.clone(), request_body_limit))
+            .bind((server.host.as_str(), server.port))?
+            .workers(server.workers)
+            .shutdown_timeout(server.shutdown_timeout);
+
+    #[cfg(feature = "tls")]
+    let server = match server.tls {
+        None => server_builder.run(),
+        Some(tls_conf) => {
+            let cert_file =
+                &mut std::io::BufReader::new(std::fs::File::open(tls_conf.certificate).map_err(
+                    |err| errors::ApplicationError::InvalidConfigurationValueError(err.to_string()),
+                )?);
+            let key_file =
+                &mut std::io::BufReader::new(std::fs::File::open(tls_conf.private_key).map_err(
+                    |err| errors::ApplicationError::InvalidConfigurationValueError(err.to_string()),
+                )?);
+
+            let cert_chain = rustls_pemfile::certs(cert_file)
+                .collect::<Result<Vec<_>, _>>()
+                .map_err(|err| {
+                    errors::ApplicationError::InvalidConfigurationValueError(err.to_string())
+                })?;
+
+            let mut keys = rustls_pemfile::pkcs8_private_keys(key_file)
+                .map(|key| key.map(rustls::pki_types::PrivateKeyDer::Pkcs8))
+                .collect::<Result<Vec<_>, _>>()
+                .map_err(|err| {
+                    errors::ApplicationError::InvalidConfigurationValueError(err.to_string())
+                })?;
+
+            // exit if no keys could be parsed
+            if keys.is_empty() {
+                return Err(errors::ApplicationError::InvalidConfigurationValueError(
+                    "Could not locate PKCS8 private keys.".into(),
+                ));
+            }
+
+            let config_builder = rustls::ServerConfig::builder().with_no_client_auth();
+            let config = config_builder
+                .with_single_cert(cert_chain, keys.remove(0))
+                .map_err(|err| {
+                    errors::ApplicationError::InvalidConfigurationValueError(err.to_string())
+                })?;
+
+            server_builder
+                .bind_rustls_0_22(
+                    (tls_conf.host.unwrap_or(server.host).as_str(), tls_conf.port),
+                    config,
+                )?
+                .run()
+        }
+    };
+
+    #[cfg(not(feature = "tls"))]
+    let server = server_builder.run();
+
     let _task_handle = tokio::spawn(receiver_for_error(rx, server.handle()).in_current_span());
     Ok(server)
 }

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [ ] New feature
- [x] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->

Added support for TLS server within `actix-web` using `rustls`

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
Closes #3475.

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
![tls](https://github.com/juspay/hyperswitch/assets/130567821/ee01116b-f353-4453-9e4a-9b1f9712207c)

Test Parameters:

- HTTP Server: 8080 (default)
- HTTPS Server: 8081
- Cert and Key generated using `mkcert` for `localhost` and `127.0.0.1`: `mkcert -key-file key.pem -cert-file cert.pem 127.0.0.1 localhost`


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
