Skip to main content

kamu_snap_crypto_axum/
lib.rs

1//! axum/tower inbound-verify glue for SNAP BI service signatures.
2//!
3//! Provides [`verify_request`] — the framework-agnostic SNAP BI verify
4//! function operating on `http::request::Parts` + body bytes. axum's `Parts`
5//! gives clean access to method/headers without consuming the body, so
6//! consumers can extract via `axum::body::Bytes` (or `axum::body::to_bytes`)
7//! and then call this function inside an extractor / handler.
8//!
9//! A full `tower::Layer` wrapper is intentionally deferred to a v2.x release;
10//! body extraction in a layered Service requires careful buffer-and-replay
11//! that's better designed once a production caller exists.
12
13#![forbid(unsafe_code)]
14
15use http::request::Parts;
16use kamu_snap_crypto::Signature;
17use kamu_snap_crypto::snap_bi::{ServiceStringToSign, verify_service};
18
19const X_SIGNATURE: &str = "X-SIGNATURE";
20const X_TIMESTAMP: &str = "X-TIMESTAMP";
21const AUTHORIZATION: &str = "Authorization";
22
23/// Verify a SNAP BI service request against `client_secret`.
24///
25/// Reads `X-SIGNATURE`, `X-TIMESTAMP`, and `Authorization` from
26/// `parts.headers`; uses `parts.method` and `parts.uri.path()` for the
27/// canonical stringToSign; hashes the supplied body bytes for the body-hash
28/// slot.
29pub fn verify_request(parts: &Parts, body: &[u8], client_secret: &str) -> kamu_snap_crypto::Result<()> {
30    let signature_b64 = header_str(&parts.headers, X_SIGNATURE)?;
31    let timestamp = header_str(&parts.headers, X_TIMESTAMP)?;
32    let authorization = header_str(&parts.headers, AUTHORIZATION)?;
33    let access_token = authorization.strip_prefix("Bearer ").unwrap_or(authorization);
34
35    let parts_canonical =
36        ServiceStringToSign { method: &parts.method, path: parts.uri.path(), access_token, body, timestamp };
37
38    let sig = Signature::from_base64(signature_b64)?;
39    verify_service(client_secret.as_bytes(), &parts_canonical, &sig)
40}
41
42fn header_str<'a>(headers: &'a http::HeaderMap, name: &'static str) -> kamu_snap_crypto::Result<&'a str> {
43    headers
44        .get(name)
45        .ok_or_else(|| kamu_snap_crypto::Error::Webhook(format!("missing header: {name}")))?
46        .to_str()
47        .map_err(|e| kamu_snap_crypto::Error::Webhook(format!("non-ASCII header {name}: {e}")))
48}