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   88619 use strict;
  1         1  
  1         45  
4 1     1   3 use warnings;
  1         5  
  1         46  
5              
6 1     1   486 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
  1         4004  
  1         111  
7 1     1   484 use MIME::Base64 qw(encode_base64 decode_base64);
  1         765  
  1         73  
8              
9 1     1   6 use Exporter 'import';
  1         0  
  1         417  
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 2598 my ($body, $signature, $secret) = @_;
18              
19 2         23 my $expected = hmac_sha256_hex($body, $secret);
20 2         4 my $sig = $signature;
21 2         10 $sig =~ s/^sha256=//;
22              
23 2         9 return _timing_safe_eq($expected, $sig);
24             }
25              
26             sub verify_event_signature {
27 2     2 1 8 my ($body, $timestamp, $signature, $secret) = @_;
28              
29 2         5 my $signature_base = "${timestamp}.${body}";
30 2         19 my $expected = hmac_sha256_hex($signature_base, $secret);
31 2         5 my $sig = $signature;
32 2         10 $sig =~ s/^sha256=//;
33              
34 2         5 return _timing_safe_eq($expected, $sig);
35             }
36              
37             sub verify_svix_signature {
38 2     2 1 8 my ($body, $svix_id, $timestamp, $signature, $secret) = @_;
39              
40 2         6 my $signature_base = "${svix_id}.${timestamp}.${body}";
41 2         8 my $secret_bytes = pack('H*', $secret);
42 2         22 my $expected_bytes = hmac_sha256($signature_base, $secret_bytes);
43 2         7 my $expected = encode_base64($expected_bytes, '');
44              
45 2         4 my $sig = $signature;
46 2         11 $sig =~ s/^v1,//;
47              
48 2         8 my $sig_bytes = decode_base64($sig);
49 2         6 my $exp_bytes = decode_base64($expected);
50              
51 2         7 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   11 my ($a, $b) = @_;
57 4 100       24 return 0 if length($a) != length($b);
58 2         4 my $result = 0;
59 2         8 for my $i (0 .. length($a) - 1) {
60 128         259 $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1));
61             }
62 2         21 return $result == 0;
63             }
64              
65             # Constant-time byte string comparison.
66             sub _timing_safe_eq_bytes {
67 2     2   6 my ($a, $b) = @_;
68 2 100       13 return 0 if length($a) != length($b);
69 1         3 my $result = 0;
70 1         5 for my $i (0 .. length($a) - 1) {
71 32         68 $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1));
72             }
73 1         8 return $result == 0;
74             }
75              
76             1;
77              
78             __END__