confish 0.1.0

Official Rust SDK for confish — typed configuration, actions, and webhooks.
Documentation
use confish::webhook::{verify, VerifyOptions, DEFAULT_TOLERANCE};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::time::Duration;

fn sign(secret: &str, ts: u64, body: &[u8]) -> String {
    let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
    mac.update(format!("{ts}:").as_bytes());
    mac.update(body);
    hex::encode(mac.finalize().into_bytes())
}

#[test]
fn accepts_valid_signature() {
    let body = br#"{"event":"environment.updated"}"#;
    let ts = 1_700_000_000;
    let sig = sign("whsec_test", ts, body);
    let header = format!("ts={ts};sig={sig}");
    let opts = VerifyOptions {
        tolerance: DEFAULT_TOLERANCE,
        now: Some(ts),
    };
    assert!(verify(body, Some(&header), "whsec_test", &opts));
}

#[test]
fn rejects_wrong_secret() {
    let body = br#"{}"#;
    let ts = 1_700_000_000;
    let sig = sign("other", ts, body);
    let header = format!("ts={ts};sig={sig}");
    let opts = VerifyOptions {
        tolerance: DEFAULT_TOLERANCE,
        now: Some(ts),
    };
    assert!(!verify(body, Some(&header), "whsec_test", &opts));
}

#[test]
fn rejects_tampered_body() {
    let secret = "whsec_test";
    let ts = 1_700_000_000;
    let sig = sign(secret, ts, br#"{"a":1}"#);
    let header = format!("ts={ts};sig={sig}");
    let opts = VerifyOptions {
        tolerance: DEFAULT_TOLERANCE,
        now: Some(ts),
    };
    assert!(!verify(br#"{"a":2}"#, Some(&header), secret, &opts));
}

#[test]
fn rejects_stale_timestamp() {
    let secret = "whsec_test";
    let ts = 1_700_000_000;
    let sig = sign(secret, ts, b"{}");
    let header = format!("ts={ts};sig={sig}");
    let opts = VerifyOptions {
        tolerance: Duration::from_secs(300),
        now: Some(ts + 600),
    };
    assert!(!verify(b"{}", Some(&header), secret, &opts));
}

#[test]
fn accepts_stale_when_tolerance_disabled() {
    let secret = "whsec_test";
    let ts = 1_700_000_000;
    let sig = sign(secret, ts, b"{}");
    let header = format!("ts={ts};sig={sig}");
    let opts = VerifyOptions {
        tolerance: Duration::ZERO,
        now: Some(ts + 99_999),
    };
    assert!(verify(b"{}", Some(&header), secret, &opts));
}

#[test]
fn rejects_malformed_headers() {
    for header in ["", "garbage", "ts=abc;sig=def", "ts=1;sig="] {
        assert!(
            !verify(b"{}", Some(header), "whsec_test", &VerifyOptions::default()),
            "expected rejection for header {header:?}"
        );
    }
    assert!(!verify(
        b"{}",
        None,
        "whsec_test",
        &VerifyOptions::default()
    ));
}