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   105886 use strict;
  1         1  
  1         29  
4 1     1   3 use warnings;
  1         6  
  1         54  
5              
6 1     1   516 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
  1         2646  
  1         86  
7 1     1   427 use MIME::Base64 qw(encode_base64 decode_base64);
  1         786  
  1         76  
8              
9 1     1   7 use Exporter 'import';
  1         1  
  1         492  
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 1425 my ($body, $signature, $secret) = @_;
18              
19 2         71 my $expected = hmac_sha256_hex($body, $secret);
20 2         6 my $sig = $signature;
21 2         6 $sig =~ s/^sha256=//;
22              
23 2         6 return _timing_safe_eq($expected, $sig);
24             }
25              
26             sub verify_event_signature {
27 2     2 1 7 my ($body, $timestamp, $signature, $secret) = @_;
28              
29 2         4 my $signature_base = "${timestamp}.${body}";
30 2         17 my $expected = hmac_sha256_hex($signature_base, $secret);
31 2         5 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         33 my $signature_base = "${svix_id}.${timestamp}.${body}";
41 2         13 my $secret_bytes = pack('H*', $secret);
42 2         20 my $expected_bytes = hmac_sha256($signature_base, $secret_bytes);
43 2         7 my $expected = encode_base64($expected_bytes, '');
44              
45 2         5 my $sig = $signature;
46 2         7 $sig =~ s/^v1,//;
47              
48 2         6 my $sig_bytes = decode_base64($sig);
49 2         6 my $exp_bytes = decode_base64($expected);
50              
51 2         5 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       19 return 0 if length($a) != length($b);
58 2         3 my $result = 0;
59 2         7 for my $i (0 .. length($a) - 1) {
60 128         173 $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1));
61             }
62 2         10 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       12 return 0 if length($a) != length($b);
69 1         2 my $result = 0;
70 1         3 for my $i (0 .. length($a) - 1) {
71 32         38 $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1));
72             }
73 1         4 return $result == 0;
74             }
75              
76             1;
77              
78             __END__