use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, PartialEq, Eq)]
pub enum SignatureError {
MalformedHeader,
Mismatch,
}
pub fn verify_signature(secret: &[u8], header: &str, body: &[u8]) -> Result<(), SignatureError> {
let prefix = "sha256=";
let hex_digest = match header.strip_prefix(prefix) {
Some(rest) => rest.trim(),
None => return Err(SignatureError::MalformedHeader),
};
if hex_digest.is_empty() {
return Err(SignatureError::MalformedHeader);
}
let received = match hex::decode(hex_digest) {
Ok(bytes) => bytes,
Err(_) => return Err(SignatureError::MalformedHeader),
};
if received.len() != 32 {
return Err(SignatureError::MalformedHeader);
}
let mut mac = HmacSha256::new_from_slice(secret).map_err(|_| SignatureError::Mismatch)?;
mac.update(body);
mac.verify_slice(&received)
.map_err(|_| SignatureError::Mismatch)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventKind {
Installation,
PullRequest,
Ping,
Other,
}
impl EventKind {
pub fn from_header(value: &str) -> Self {
match value {
"installation" | "installation_repositories" => Self::Installation,
"pull_request" => Self::PullRequest,
"ping" => Self::Ping,
_ => Self::Other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const SECRET: &[u8] = b"sekret";
const BODY: &[u8] = b"hello";
const KNOWN_DIGEST: &str = "24de3247aa41906931f59dd849ce2bf66043c21955d9f4726c198ee3006c5f47";
fn ok_header() -> String {
format!("sha256={KNOWN_DIGEST}")
}
#[test]
fn verify_accepts_known_signature() {
assert_eq!(verify_signature(SECRET, &ok_header(), BODY), Ok(()));
}
#[test]
fn verify_rejects_modified_body() {
assert_eq!(
verify_signature(SECRET, &ok_header(), b"hellO"),
Err(SignatureError::Mismatch)
);
}
#[test]
fn verify_rejects_wrong_secret() {
assert_eq!(
verify_signature(b"wrong", &ok_header(), BODY),
Err(SignatureError::Mismatch)
);
}
#[test]
fn verify_rejects_missing_prefix() {
assert_eq!(
verify_signature(SECRET, KNOWN_DIGEST, BODY),
Err(SignatureError::MalformedHeader)
);
}
#[test]
fn verify_rejects_empty_digest() {
assert_eq!(
verify_signature(SECRET, "sha256=", BODY),
Err(SignatureError::MalformedHeader)
);
}
#[test]
fn verify_rejects_non_hex() {
assert_eq!(
verify_signature(SECRET, "sha256=zzzz", BODY),
Err(SignatureError::MalformedHeader)
);
}
#[test]
fn verify_rejects_short_digest() {
assert_eq!(
verify_signature(SECRET, "sha256=00112233445566778899aabbccddeeff", BODY),
Err(SignatureError::MalformedHeader)
);
}
#[test]
fn verify_handles_trimming() {
let with_ws = format!("sha256={KNOWN_DIGEST} ");
assert_eq!(verify_signature(SECRET, &with_ws, BODY), Ok(()));
}
#[test]
fn event_kind_maps_known_headers() {
assert_eq!(
EventKind::from_header("pull_request"),
EventKind::PullRequest
);
assert_eq!(
EventKind::from_header("installation"),
EventKind::Installation
);
assert_eq!(
EventKind::from_header("installation_repositories"),
EventKind::Installation
);
assert_eq!(EventKind::from_header("ping"), EventKind::Ping);
}
#[test]
fn event_kind_unknown_falls_back_to_other() {
assert_eq!(EventKind::from_header("workflow_run"), EventKind::Other);
assert_eq!(EventKind::from_header(""), EventKind::Other);
}
}