Apertur-SDK

 view release on metacpan or  search on metacpan

lib/Apertur/SDK/Signature.pm  view on Meta::CPAN

package Apertur::SDK::Signature;

use strict;
use warnings;

use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
use MIME::Base64 qw(encode_base64 decode_base64);

use Exporter 'import';
our @EXPORT_OK = qw(
    verify_webhook_signature
    verify_event_signature
    verify_svix_signature
);

sub verify_webhook_signature {
    my ($body, $signature, $secret) = @_;

    my $expected = hmac_sha256_hex($body, $secret);
    my $sig = $signature;
    $sig =~ s/^sha256=//;

    return _timing_safe_eq($expected, $sig);
}

sub verify_event_signature {
    my ($body, $timestamp, $signature, $secret) = @_;

    my $signature_base = "${timestamp}.${body}";
    my $expected = hmac_sha256_hex($signature_base, $secret);
    my $sig = $signature;
    $sig =~ s/^sha256=//;

    return _timing_safe_eq($expected, $sig);
}

sub verify_svix_signature {
    my ($body, $svix_id, $timestamp, $signature, $secret) = @_;

    my $signature_base = "${svix_id}.${timestamp}.${body}";
    my $secret_bytes = pack('H*', $secret);
    my $expected_bytes = hmac_sha256($signature_base, $secret_bytes);
    my $expected = encode_base64($expected_bytes, '');

    my $sig = $signature;
    $sig =~ s/^v1,//;

    my $sig_bytes   = decode_base64($sig);
    my $exp_bytes   = decode_base64($expected);

    return _timing_safe_eq_bytes($exp_bytes, $sig_bytes);
}

# Constant-time string comparison to prevent timing attacks.
sub _timing_safe_eq {
    my ($a, $b) = @_;
    return 0 if length($a) != length($b);
    my $result = 0;
    for my $i (0 .. length($a) - 1) {
        $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1));
    }
    return $result == 0;
}

# Constant-time byte string comparison.
sub _timing_safe_eq_bytes {
    my ($a, $b) = @_;
    return 0 if length($a) != length($b);
    my $result = 0;
    for my $i (0 .. length($a) - 1) {
        $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1));
    }
    return $result == 0;
}

1;

__END__

=head1 NAME

Apertur::SDK::Signature - Webhook signature verification

=head1 SYNOPSIS

    use Apertur::SDK::Signature qw(
        verify_webhook_signature
        verify_event_signature
        verify_svix_signature
    );

    # Image delivery webhook
    my $valid = verify_webhook_signature($body, $signature, $secret);

    # Event webhook (HMAC method)
    my $valid = verify_event_signature($body, $timestamp, $signature, $secret);

    # Event webhook (Svix method)
    my $valid = verify_svix_signature($body, $svix_id, $timestamp, $signature, $secret);

=head1 DESCRIPTION

Provides functions to verify webhook signatures sent by the Apertur API.
All comparisons use constant-time algorithms to prevent timing attacks.

=head1 FUNCTIONS

=over 4

=item B<verify_webhook_signature($body, $signature, $secret)>

Verifies an image delivery webhook. The signature is expected to be in
the format C<sha256=E<lt>hexE<gt>>.

=item B<verify_event_signature($body, $timestamp, $signature, $secret)>

Verifies an event webhook using the HMAC method. The signed payload is
C<${timestamp}.${body}>.

=item B<verify_svix_signature($body, $svix_id, $timestamp, $signature, $secret)>

Verifies an event webhook using the Svix method. The signed payload is
C<${svix_id}.${timestamp}.${body}> and the secret is hex-decoded before
use. The signature is expected in the format C<v1,E<lt>base64E<gt>>.

=back

=cut



( run in 0.481 second using v1.01-cache-2.11-cpan-e1769b4cff6 )