From 8df3afe75e8a284d36d9d29668320e3a9b9607d4 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 3 Feb 2025 20:50:25 +0100 Subject: [PATCH] Include payload in integrity controls --- Cargo.lock | 108 ++++++++++++++++++++++++++-------- Cargo.toml | 5 +- examples/api_curl.rs | 5 ++ src/extractors/client_auth.rs | 57 ++++++++++++++++-- 4 files changed, 145 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58e5684..fb2b556 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,7 +235,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] @@ -577,6 +577,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd016a0ddc7cb13661bf5576073ce07330a693f8608a1320b4e20561cc12cdc" +dependencies = [ + "hybrid-array", +] + [[package]] name = "brotli" version = "6.0.0" @@ -671,7 +680,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", ] @@ -765,6 +774,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ff6be19477a1bd5441f382916a89bc2a0b2c35db6d41e0f6e8538bf6d6463f" + [[package]] name = "const-random" version = "0.1.18" @@ -809,7 +824,7 @@ dependencies = [ "hmac", "percent-encoding", "rand 0.8.5", - "sha2", + "sha2 0.10.8", "subtle", "time", "version_check", @@ -878,6 +893,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b8ce8218c97789f16356e7896b3714f26c2ee1079b79c0b7ae7064bb9089fa" +dependencies = [ + "getrandom 0.2.15", + "hybrid-array", + "rand_core 0.6.4", +] + [[package]] name = "ct-codecs" version = "1.1.3" @@ -899,7 +925,7 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -954,12 +980,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.0-pre.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2e3d6615d99707295a9673e889bf363a04b2a466bd320c65a72536f7577379" +dependencies = [ + "block-buffer 0.11.0-rc.3", + "const-oid 0.10.0-rc.3", + "crypto-common 0.2.0-rc.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -987,7 +1024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -1012,7 +1049,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -1361,7 +1398,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1376,7 +1413,7 @@ version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a8575493d277c9092b988c780c94737fb9fd8651a1001e16bee3eccfc1baedb" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1385,7 +1422,7 @@ version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0b3a0f572aa8389d325f5852b9e0a333a15b0f86ecccbb3fdb6e97cd86dc67c" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1469,6 +1506,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.5.2" @@ -1804,7 +1850,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2", + "sha2 0.10.8", "signature", ] @@ -1903,6 +1949,8 @@ dependencies = [ "actix-web", "anyhow", "askama", + "base16ct", + "bytes", "chrono", "clap", "env_logger", @@ -1918,6 +1966,7 @@ dependencies = [ "rust-s3", "serde", "serde_json", + "sha2 0.11.0-pre.4", "thiserror 2.0.11", "urlencoding", "uuid", @@ -2172,7 +2221,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -2184,7 +2233,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.8", ] [[package]] @@ -2569,15 +2618,15 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", "rand_core 0.6.4", - "sha2", + "sha2 0.10.8", "signature", "spki", "subtle", @@ -2614,7 +2663,7 @@ version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" dependencies = [ - "sha2", + "sha2 0.10.8", "walkdir", ] @@ -2655,7 +2704,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2", + "sha2 0.10.8", "thiserror 1.0.69", "time", "tokio", @@ -2880,7 +2929,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -2891,7 +2940,18 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-pre.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "540c0893cce56cdbcfebcec191ec8e0f470dd1889b6e7a0b503e310a94a168f5" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-pre.9", ] [[package]] @@ -2915,7 +2975,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -3314,7 +3374,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] diff --git a/Cargo.toml b/Cargo.toml index 7599277..769eca6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,4 +26,7 @@ ipnet = { version = "2.11.0", features = ["serde"] } chrono = "0.4.39" futures-util = "0.3.31" jwt-simple = { version = "0.12.11", default-features=false, features=["pure-rust"] } -actix-remote-ip = "0.1.0" \ No newline at end of file +actix-remote-ip = "0.1.0" +bytes = "1.9.0" +sha2 = "0.11.0-pre.4" +base16ct = "0.2.0" \ No newline at end of file diff --git a/examples/api_curl.rs b/examples/api_curl.rs index 3f078b0..6e4a626 100644 --- a/examples/api_curl.rs +++ b/examples/api_curl.rs @@ -31,6 +31,10 @@ struct Args { #[arg(short('X'), long, default_value = "GET")] method: String, + /// Payload SHA256 digest + #[arg(short('D'), long)] + payload_digest: Option, + /// Request URI uri: String, @@ -59,6 +63,7 @@ fn main() { custom: TokenClaims { method: args.method.to_string(), uri: args.uri, + payload_sha256: args.payload_digest.clone(), }, }; diff --git a/src/extractors/client_auth.rs b/src/extractors/client_auth.rs index 0f446ec..2321361 100644 --- a/src/extractors/client_auth.rs +++ b/src/extractors/client_auth.rs @@ -3,25 +3,34 @@ use crate::utils::curr_time; use actix_remote_ip::RemoteIP; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest}; +use bytes::Bytes; use jwt_simple::common::VerificationOptions; use jwt_simple::prelude::{Duration, HS256Key, MACLike}; +use sha2::{Digest, Sha256}; use std::net::IpAddr; use std::str::FromStr; pub struct APIClientAuth { pub user: UserConfig, client: APIClient, - payload: Option>, + pub payload: Option>, } #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct TokenClaims { + #[serde(rename = "met")] pub method: String, pub uri: String, + #[serde(rename = "pay", skip_serializing_if = "Option::is_none")] + pub payload_sha256: Option, } impl APIClientAuth { - async fn extract_auth(req: &HttpRequest, remote_ip: IpAddr) -> Result { + async fn extract_auth( + req: &HttpRequest, + remote_ip: IpAddr, + payload_bytes: Option, + ) -> Result { let Some(token) = req.headers().get("x-client-auth") else { return Err(actix_web::error::ErrorBadRequest( "Missing authentication header!", @@ -129,7 +138,27 @@ impl APIClientAuth { )); } - // TODO : handle payload + let payload = match (payload_bytes, claims.custom.payload_sha256) { + (None, _) => None, + (Some(_), None) => { + return Err(actix_web::error::ErrorBadRequest( + "A payload digest must be included in the JWT when the request has a payload!", + )); + } + (Some(payload), Some(provided_digest)) => { + let computed_digest = base16ct::lower::encode_string(&Sha256::digest(&payload)); + if computed_digest != provided_digest { + log::error!( + "Expected digest {provided_digest} but computed {computed_digest}!" + ); + return Err(actix_web::error::ErrorBadRequest( + "Computed digest is different from the one provided in the JWT!", + )); + } + + Some(payload.to_vec()) + } + }; // Update last use (if needed) if client.need_update_last_used() { @@ -145,7 +174,7 @@ impl APIClientAuth { Ok(Self { client: client.clone(), - payload: None, + payload, user, }) } @@ -163,6 +192,24 @@ impl FromRequest for APIClientAuth { Err(e) => return Box::pin(async { Err(e) }), }; - Box::pin(async move { Self::extract_auth(&req, remote_ip.0).await }) + let mut payload = payload.take(); + + Box::pin(async move { + let payload_bytes = match Bytes::from_request(&req, &mut payload).await { + Ok(b) => { + if b.is_empty() { + None + } else { + Some(b) + } + } + Err(e) => { + log::error!("Failed to extract request payload! {e}"); + None + } + }; + + Self::extract_auth(&req, remote_ip.0, payload_bytes).await + }) } }