Skip to content

Commit 643ced0

Browse files
committed
refactor(trogon-github): replace bool signature check with SignatureError enum
Enables structured logging with specific failure reasons (missing prefix, invalid hex, mismatch). Also adds NATS healthcheck to docker-compose. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 3ef774b commit 643ced0

4 files changed

Lines changed: 68 additions & 24 deletions

File tree

rsworkspace/crates/trogon-github/docker-compose.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ name: trogon-github
33
services:
44
nats:
55
image: nats:2-alpine
6-
command: ["--jetstream", "--store_dir=/data"]
6+
command: ["--jetstream", "--store_dir=/data", "--http_port=8222"]
77
ports:
88
- "4222:4222"
99
- "8222:8222"
1010
volumes:
1111
- nats-data:/data
1212
restart: unless-stopped
13+
healthcheck:
14+
test: ["CMD", "wget", "-qO-", "http://localhost:8222/healthz"]
15+
interval: 5s
16+
timeout: 3s
17+
start_period: 5s
18+
retries: 3
1319

1420
trogon-github:
1521
build:
@@ -28,7 +34,8 @@ services:
2834
GITHUB_MAX_BODY_SIZE: "${GITHUB_MAX_BODY_SIZE:-26214400}"
2935
RUST_LOG: "${RUST_LOG:-info}"
3036
depends_on:
31-
- nats
37+
nats:
38+
condition: service_healthy
3239
restart: unless-stopped
3340
healthcheck:
3441
test: ["CMD", "curl", "-sf", "http://localhost:${GITHUB_WEBHOOK_PORT:-8080}/health"]

rsworkspace/crates/trogon-github/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ pub mod signature;
3737

3838
pub use config::GithubConfig;
3939
pub use server::{ServeError, provision};
40+
pub use signature::SignatureError;
4041
#[cfg(not(coverage))]
4142
pub use server::{router, serve};

rsworkspace/crates/trogon-github/src/server.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,11 @@ async fn handle_webhook<P: JetStreamPublisher>(
219219
.and_then(|v| v.to_str().ok());
220220

221221
match sig {
222-
Some(sig) if signature::verify(secret, &body, sig) => {}
223-
Some(_) => {
224-
warn!("Invalid GitHub webhook signature");
225-
return StatusCode::UNAUTHORIZED;
222+
Some(sig) => {
223+
if let Err(e) = signature::verify(secret, &body, sig) {
224+
warn!(reason = %e, "GitHub webhook signature validation failed");
225+
return StatusCode::UNAUTHORIZED;
226+
}
226227
}
227228
None => {
228229
warn!("Missing X-Hub-Signature-256 header");
Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
1+
use std::fmt;
2+
13
use hmac::{Hmac, Mac};
24
use sha2::Sha256;
35

46
type HmacSha256 = Hmac<Sha256>;
57

8+
#[derive(Debug)]
9+
pub enum SignatureError {
10+
MissingPrefix,
11+
InvalidHex,
12+
Mismatch,
13+
}
14+
15+
impl fmt::Display for SignatureError {
16+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17+
match self {
18+
SignatureError::MissingPrefix => f.write_str("missing sha256= prefix"),
19+
SignatureError::InvalidHex => f.write_str("invalid hex encoding"),
20+
SignatureError::Mismatch => f.write_str("signature mismatch"),
21+
}
22+
}
23+
}
24+
625
/// Verifies a GitHub webhook signature using constant-time comparison.
726
///
827
/// GitHub sends `X-Hub-Signature-256: sha256=<hex>`. This function validates
928
/// the HMAC-SHA256 of the raw request body against that header value.
10-
pub fn verify(secret: &str, body: &[u8], signature_header: &str) -> bool {
11-
let hex_sig = match signature_header.strip_prefix("sha256=") {
12-
Some(s) => s,
13-
None => return false,
14-
};
29+
pub fn verify(secret: &str, body: &[u8], signature_header: &str) -> Result<(), SignatureError> {
30+
let hex_sig = signature_header
31+
.strip_prefix("sha256=")
32+
.ok_or(SignatureError::MissingPrefix)?;
1533

16-
let Ok(expected) = hex::decode(hex_sig) else {
17-
return false;
18-
};
34+
let expected = hex::decode(hex_sig).map_err(|_| SignatureError::InvalidHex)?;
1935

20-
let Ok(mut mac) = HmacSha256::new_from_slice(secret.as_bytes()) else {
21-
return false;
22-
};
36+
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
37+
.expect("HMAC-SHA256 accepts any key length");
2338

2439
mac.update(body);
25-
mac.verify_slice(&expected).is_ok()
40+
mac.verify_slice(&expected)
41+
.map_err(|_| SignatureError::Mismatch)
2642
}
2743

2844
#[cfg(test)]
@@ -35,39 +51,58 @@ mod tests {
3551
format!("sha256={}", hex::encode(mac.finalize().into_bytes()))
3652
}
3753

54+
#[test]
55+
fn error_display_messages() {
56+
assert_eq!(SignatureError::MissingPrefix.to_string(), "missing sha256= prefix");
57+
assert_eq!(SignatureError::InvalidHex.to_string(), "invalid hex encoding");
58+
assert_eq!(SignatureError::Mismatch.to_string(), "signature mismatch");
59+
}
60+
3861
#[test]
3962
fn valid_signature_passes() {
4063
let sig = compute_sig("test-secret", b"hello world");
41-
assert!(verify("test-secret", b"hello world", &sig));
64+
assert!(verify("test-secret", b"hello world", &sig).is_ok());
4265
}
4366

4467
#[test]
4568
fn wrong_secret_fails() {
4669
let sig = compute_sig("correct-secret", b"body");
47-
assert!(!verify("wrong-secret", b"body", &sig));
70+
assert!(matches!(
71+
verify("wrong-secret", b"body", &sig),
72+
Err(SignatureError::Mismatch)
73+
));
4874
}
4975

5076
#[test]
5177
fn tampered_body_fails() {
5278
let sig = compute_sig("secret", b"original body");
53-
assert!(!verify("secret", b"tampered body", &sig));
79+
assert!(matches!(
80+
verify("secret", b"tampered body", &sig),
81+
Err(SignatureError::Mismatch)
82+
));
5483
}
5584

5685
#[test]
5786
fn missing_sha256_prefix_fails() {
5887
let sig = compute_sig("secret", b"body");
5988
let raw_hex = sig.strip_prefix("sha256=").unwrap().to_string();
60-
assert!(!verify("secret", b"body", &raw_hex));
89+
assert!(matches!(
90+
verify("secret", b"body", &raw_hex),
91+
Err(SignatureError::MissingPrefix)
92+
));
6193
}
6294

6395
#[test]
6496
fn invalid_hex_fails() {
65-
assert!(!verify("secret", b"body", "sha256=not-valid-hex!"));
97+
assert!(matches!(
98+
verify("secret", b"body", "sha256=not-valid-hex!"),
99+
Err(SignatureError::InvalidHex)
100+
));
66101
}
67102

68103
#[test]
69104
fn empty_body_with_valid_sig_passes() {
70105
let sig = compute_sig("secret", b"");
71-
assert!(verify("secret", b"", &sig));
106+
assert!(verify("secret", b"", &sig).is_ok());
72107
}
73108
}

0 commit comments

Comments
 (0)