Skip to main content

kamu_snap_crypto_actix/
lib.rs

1//! actix-web inbound-verify glue for SNAP BI service signatures.
2//!
3//! Provides [`verify_request`] — a single function that takes the parts of an
4//! `actix-web` request (method, path, headers, body) plus a client secret,
5//! and returns `Ok(())` iff the incoming `X-SIGNATURE` validates against the
6//! canonical SNAP BI service `stringToSign`.
7//!
8//! A full `Transform`/middleware wrapper is intentionally deferred to a v2.x
9//! release — body extraction inside actix middleware requires
10//! buffer-and-replay plumbing that's better designed once we have a
11//! production caller. For now, consumers call [`verify_request`] from inside
12//! their own handler (or a custom `FromRequest` extractor) after the body is
13//! materialised.
14
15#![forbid(unsafe_code)]
16
17use actix_web::http::{Method as ActixMethod, header::HeaderMap as ActixHeaderMap};
18use kamu_snap_crypto::Signature;
19use kamu_snap_crypto::snap_bi::{ServiceStringToSign, verify_service};
20
21const X_SIGNATURE: &str = "X-SIGNATURE";
22const X_TIMESTAMP: &str = "X-TIMESTAMP";
23const AUTHORIZATION: &str = "Authorization";
24
25/// Verify a SNAP BI service request against `client_secret`. Returns `Ok(())`
26/// when `X-SIGNATURE` matches the canonical stringToSign computed from the
27/// supplied method, path, body, `X-TIMESTAMP` header, and Bearer access
28/// token. Any missing header or signature mismatch yields a
29/// [`kamu_snap_crypto::Error`].
30pub fn verify_request(
31    method: &ActixMethod,
32    path: &str,
33    headers: &ActixHeaderMap,
34    body: &[u8],
35    client_secret: &str,
36) -> kamu_snap_crypto::Result<()> {
37    let signature_b64 = header_str(headers, X_SIGNATURE)?;
38    let timestamp = header_str(headers, X_TIMESTAMP)?;
39    let authorization = header_str(headers, AUTHORIZATION)?;
40    let access_token = authorization.strip_prefix("Bearer ").unwrap_or(authorization);
41
42    let http_method = http::Method::from_bytes(method.as_str().as_bytes())
43        .map_err(|e| kamu_snap_crypto::Error::Webhook(format!("invalid HTTP method: {e}")))?;
44
45    let parts = ServiceStringToSign { method: &http_method, path, access_token, body, timestamp };
46
47    let sig = Signature::from_base64(signature_b64)?;
48    verify_service(client_secret.as_bytes(), &parts, &sig)
49}
50
51fn header_str<'a>(headers: &'a ActixHeaderMap, name: &'static str) -> kamu_snap_crypto::Result<&'a str> {
52    headers
53        .get(name)
54        .ok_or_else(|| kamu_snap_crypto::Error::Webhook(format!("missing header: {name}")))?
55        .to_str()
56        .map_err(|e| kamu_snap_crypto::Error::Webhook(format!("non-ASCII header {name}: {e}")))
57}