Skip to content

Commit

Permalink
funds-manager: funds-manager-api: Publish API auth helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
joeykraut committed Aug 5, 2024
1 parent d2b6066 commit 671d08a
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 44 deletions.
4 changes: 4 additions & 0 deletions funds-manager/funds-manager-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ edition = "2021"
[dependencies]

renegade-api = { package = "external-api", workspace = true }
hmac = "0.12.1"
http = "0.2.12"
itertools = "0.13.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.117"
sha2 = "0.10.7"
uuid = "1.7.1"
53 changes: 53 additions & 0 deletions funds-manager/funds-manager-api/src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! Auth helpers for the API
use hmac::{Hmac, Mac};
use http::HeaderMap;
use itertools::Itertools;
use sha2::Sha256;

/// The header key for the HMAC signature
pub const X_SIGNATURE_HEADER: &str = "X-Signature";
/// The prefix for Renegade headers, these headers are included in the HMAC
/// signature
pub const RENEGADE_HEADER_PREFIX: &str = "x-renegade-";

/// Compute an hmac for the given request
pub fn compute_hmac(
hmac_key: &[u8],
method: &str,
path: &str,
headers: &HeaderMap,
body: &[u8],
) -> Vec<u8> {
// Construct the MAC
let mut mac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("HMAC error");

// Update with method, path, headers and body in order
mac.update(method.as_bytes());
mac.update(path.as_bytes());
add_headers_to_hmac(&mut mac, headers);
mac.update(body);

// Check the signature
mac.finalize().into_bytes().to_vec()
}

/// Hash headers into an HMAC
fn add_headers_to_hmac(mac: &mut Hmac<Sha256>, headers: &HeaderMap) {
let mut renegade_headers = headers
.iter()
.filter_map(|(k, v)| {
let key = k.to_string().to_lowercase();
if key.starts_with(RENEGADE_HEADER_PREFIX) {
Some((key, v))
} else {
None
}
})
.collect_vec();
renegade_headers.sort_by(|a, b| a.0.cmp(&b.0));

for (key, value) in renegade_headers {
mac.update(key.as_bytes());
mac.update(value.as_bytes());
}
}
2 changes: 2 additions & 0 deletions funds-manager/funds-manager-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#![deny(missing_docs)]
#![deny(clippy::missing_docs_in_private_items)]

pub mod auth;

use renegade_api::types::ApiWallet;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
Expand Down
2 changes: 0 additions & 2 deletions funds-manager/funds-manager-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ check-revoke = false
clap = { version = "4.5.3", features = ["derive", "env"] }
funds-manager-api = { path = "../funds-manager-api" }
hex = "0.4.3"
hmac = "0.12.1"
http-body-util = "0.1.0"
sha2 = "0.10.6"
tokio = { version = "1.10", features = ["full"] }
warp = "0.3"

Expand Down
44 changes: 2 additions & 42 deletions funds-manager/funds-manager-server/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,11 @@
use crate::error::ApiError;
use crate::Server;
use bytes::Bytes;
use hmac::{Hmac, Mac};
use itertools::Itertools;
use funds_manager_api::auth::{compute_hmac, X_SIGNATURE_HEADER};
use serde::de::DeserializeOwned;
use sha2::Sha256;
use std::sync::Arc;
use warp::Filter;

/// The header key for the HMAC signature
const X_SIGNATURE_HEADER: &str = "X-Signature";
/// The prefix for Renegade headers, these headers are included in the HMAC
/// signature
const RENEGADE_HEADER_PREFIX: &str = "x-renegade-";

/// Add HMAC authentication to a route
pub(crate) fn with_hmac_auth(
server: Arc<Server>,
Expand Down Expand Up @@ -54,18 +46,7 @@ async fn verify_hmac(
},
};

// Construct the MAC
let mut mac = Hmac::<Sha256>::new_from_slice(hmac_key)
.map_err(|_| warp::reject::custom(ApiError::InternalError("HMAC error".to_string())))?;

// Update with method, path, headers and body in order
mac.update(method.as_str().as_bytes());
mac.update(path.as_str().as_bytes());
add_headers_to_hmac(&mut mac, &headers);
mac.update(&body);

// Check the signature
let expected = mac.finalize().into_bytes();
let expected = compute_hmac(hmac_key, method.as_str(), path.as_str(), &headers, &body);
let provided = hex::decode(signature)
.map_err(|_| warp::reject::custom(ApiError::BadRequest("Invalid signature".to_string())))?;
if expected.as_slice() != provided.as_slice() {
Expand All @@ -77,27 +58,6 @@ async fn verify_hmac(
Ok(body)
}

/// Hash headers into an HMAC
fn add_headers_to_hmac(mac: &mut Hmac<Sha256>, headers: &warp::http::HeaderMap) {
let mut renegade_headers = headers
.iter()
.filter_map(|(k, v)| {
let key = k.as_str().to_lowercase();
if key.starts_with(RENEGADE_HEADER_PREFIX) {
Some((key, v.to_str().unwrap_or("").to_string()))
} else {
None
}
})
.collect_vec();
renegade_headers.sort_by(|a, b| a.0.cmp(&b.0));

for (key, value) in renegade_headers {
mac.update(key.as_bytes());
mac.update(value.as_bytes());
}
}

/// Extract a JSON body from a request
#[allow(clippy::needless_pass_by_value)]
pub fn with_json_body<T: DeserializeOwned + Send>(body: Bytes) -> Result<T, warp::Rejection> {
Expand Down

0 comments on commit 671d08a

Please sign in to comment.