# Authoring a tool — Rust (axum)
Bare-axum guide. v0.1 has no `openlatch-tool-sdk` Rust crate yet
(deferred to v0.2 per PRD §10.1) — write the verifier inline. The
codebase's own `runtime/webhook.rs::verify` is the reference impl.
## Scaffold
```bash
openlatch-provider new tool --template rust --out my-tool
cd my-tool
cargo build
```
## Minimum viable tool
```rust
use axum::{
body::Bytes,
extract::State,
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::post,
Router,
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use subtle::ConstantTimeEq;
type HmacSha256 = Hmac<Sha256>;
#[derive(Clone)]
struct AppState {
whsec: Arc<Vec<u8>>, // already base64-decoded, whsec_ prefix stripped
}
#[tokio::main]
async fn main() {
let whsec = std::fs::read(".secret").unwrap();
let app = Router::new()
.route("/event", post(handle_event))
.with_state(AppState { whsec: Arc::new(whsec) });
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn handle_event(
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Response {
let id = headers.get("webhook-id").and_then(|v| v.to_str().ok()).unwrap_or("");
let ts = headers.get("webhook-timestamp")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
let sig = headers.get("webhook-signature").and_then(|v| v.to_str().ok()).unwrap_or("");
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
if (now - ts).abs() > 300 {
return (StatusCode::UNAUTHORIZED, "timestamp skew").into_response();
}
let mut mac = HmacSha256::new_from_slice(&state.whsec).unwrap();
mac.update(format!("{id}.{ts}.").as_bytes());
mac.update(&body);
let expected = mac.finalize().into_bytes();
let presented_b64 = sig.split(',').nth(1).unwrap_or("");
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
let presented = STANDARD.decode(presented_b64).unwrap_or_default();
if !bool::from(expected.as_slice().ct_eq(&presented)) {
return (StatusCode::UNAUTHORIZED, "signature mismatch").into_response();
}
let event: serde_json::Value = match serde_json::from_slice(&body) {
Ok(v) => v,
Err(_) => return (StatusCode::BAD_REQUEST, "malformed body").into_response(),
};
// ---- detection ----
let _ = event;
axum::Json(serde_json::json!({
"riskScore": 5,
"verdictHint": "allow",
"latencyMs": 1
}))
.into_response()
}
```
## Best practices
- **rustls only** — `openssl-sys` and `native-tls` MUST NOT appear in
your dep tree. Match `openlatch-provider`'s own posture.
- **Constant-time compare** via `subtle::ConstantTimeEq`. Never `==`.
- **Body size cap** via `tower-http::limit::RequestBodyLimitLayer`.
- **Per-binding semaphore** if your detector is CPU-heavy and you want
back-pressure independent of tokio's task pool.
## Verdict shape
See the typify-generated structs that openlatch-provider itself uses
in `src/generated/types.rs` once you've cargo-installed the schemas
locally. The shape is camelCase and matches the platform's
`ProviderCall` DTO — see `webhook-security.md` for the field table.
## v0.2 roadmap
A Rust `openlatch-tool-sdk` crate is on the v0.2 roadmap (per
PRD §10.1). It will export an `axum::Layer` that handles verification
+ verdict shaping out of the box, mirroring the Python and TS SDKs.