1+ use std:: fmt;
2+
13use hmac:: { Hmac , Mac } ;
24use sha2:: Sha256 ;
35
46type 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