gitea-sdk-rs 0.1.0

Rust SDK for the Gitea API
Documentation
// Copyright 2026 infinitete. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

//! Authentication mechanisms for the Gitea API client.

pub(crate) mod httpsig;
pub(crate) mod pageant;
pub(crate) mod ssh_agent;
pub(crate) mod ssh_sign;

use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;

type HmacSha256 = Hmac<Sha256>;

/// Verify a Gitea webhook HMAC-SHA256 signature.
///
/// Returns `Ok(true)` if the signature matches, `Ok(false)` if it doesn't,
/// or `Err` if the expected signature is not valid hex.
///
/// Uses constant-time comparison to prevent timing attacks.
pub fn verify_webhook_signature(
    secret: &str,
    expected: &str,
    payload: &[u8],
) -> crate::Result<bool> {
    let expected_bytes = hex::decode(expected)
        .map_err(|_| crate::Error::Validation("invalid hex signature".into()))?;

    let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
        .map_err(|_| crate::Error::Validation("HMAC key error".into()))?;
    mac.update(payload);
    let computed = mac.finalize().into_bytes();

    Ok(computed.ct_eq(&expected_bytes).into())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_webhook_valid() {
        let secret = "my-secret";
        let payload = b"test-payload";
        let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
        mac.update(payload);
        let expected_hex = hex::encode(mac.finalize().into_bytes());

        assert!(verify_webhook_signature(secret, &expected_hex, payload).unwrap());
    }

    #[test]
    fn test_webhook_invalid() {
        let secret = "my-secret";
        let payload = b"test-payload";
        let wrong_sig = "0000000000000000000000000000000000000000000000000000000000000000";

        assert!(!verify_webhook_signature(secret, wrong_sig, payload).unwrap());
    }

    #[test]
    fn test_webhook_bad_hex() {
        let result = verify_webhook_signature("secret", "not-hex-at-all!", b"payload");
        assert!(result.is_err());
    }

    #[test]
    fn test_webhook_wrong_length() {
        let secret = "secret";
        let payload = b"payload";
        let wrong_sig = "abcd";
        assert!(!verify_webhook_signature(secret, wrong_sig, payload).unwrap());
    }

    #[test]
    fn test_webhook_empty_secret() {
        let secret = "";
        let payload = b"test";
        let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
        mac.update(payload);
        let expected_hex = hex::encode(mac.finalize().into_bytes());

        assert!(verify_webhook_signature("", &expected_hex, payload).unwrap());
    }
}