File Coverage

blib/lib/Apertur/SDK/Signature.pm
Criterion Covered Total %
statement 48 48 100.0
branch 4 4 100.0
condition n/a
subroutine 10 10 100.0
pod 3 3 100.0
total 65 65 100.0


line stmt bran cond sub pod time code
1             package Apertur::SDK::Signature;
2              
3 1     1   94154 use strict;
  1         1  
  1         30  
4 1     1   4 use warnings;
  1         5  
  1         61  
5              
6 1     1   630 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
  1         3738  
  1         88  
7 1     1   473 use MIME::Base64 qw(encode_base64 decode_base64);
  1         784  
  1         71  
8              
9 1     1   5 use Exporter 'import';
  1         2  
  1         468  
10             our @EXPORT_OK = qw(
11             verify_webhook_signature
12             verify_event_signature
13             verify_svix_signature
14             );
15              
16             sub verify_webhook_signature {
17 2     2 1 1837 my ($body, $signature, $secret) = @_;
18              
19 2         22 my $expected = hmac_sha256_hex($body, $secret);
20 2         3 my $sig = $signature;
21 2         8 $sig =~ s/^sha256=//;
22              
23 2         6 return _timing_safe_eq($expected, $sig);
24             }
25              
26             sub verify_event_signature {
27 2     2 1 5 my ($body, $timestamp, $signature, $secret) = @_;
28              
29 2         3 my $signature_base = "${timestamp}.${body}";
30 2         14 my $expected = hmac_sha256_hex($signature_base, $secret);
31 2         3 my $sig = $signature;
32 2         7 $sig =~ s/^sha256=//;
33              
34 2         5 return _timing_safe_eq($expected, $sig);
35             }
36              
37             sub verify_svix_signature {
38 2     2 1 6 my ($body, $svix_id, $timestamp, $signature, $secret) = @_;
39              
40 2         4 my $signature_base = "${svix_id}.${timestamp}.${body}";
41 2         5 my $secret_bytes = pack('H*', $secret);
42 2         13 my $expected_bytes = hmac_sha256($signature_base, $secret_bytes);
43 2         6 my $expected = encode_base64($expected_bytes, '');
44              
45 2         4 my $sig = $signature;
46 2         7 $sig =~ s/^v1,//;
47              
48 2         4 my $sig_bytes = decode_base64($sig);
49 2         5 my $exp_bytes = decode_base64($expected);
50              
51 2         4 return _timing_safe_eq_bytes($exp_bytes, $sig_bytes);
52             }
53              
54             # Constant-time string comparison to prevent timing attacks.
55             sub _timing_safe_eq {
56 4     4   8 my ($a, $b) = @_;
57 4 100       31 return 0 if length($a) != length($b);
58 2         3 my $result = 0;
59 2         8 for my $i (0 .. length($a) - 1) {
60 128         145 $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1));
61             }
62 2         11 return $result == 0;
63             }
64              
65             # Constant-time byte string comparison.
66             sub _timing_safe_eq_bytes {
67 2     2   5 my ($a, $b) = @_;
68 2 100       9 return 0 if length($a) != length($b);
69 1         1 my $result = 0;
70 1         3 for my $i (0 .. length($a) - 1) {
71 32         39 $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1));
72             }
73 1         5 return $result == 0;
74             }
75              
76             1;
77              
78             __END__