AWS-SNS-Verify

 view release on metacpan or  search on metacpan

author.t/01_verify.t  view on Meta::CPAN

"Timestamp" : "2019-11-20T19:09:51.151Z", 
"SignatureVersion" : "1", 
"Signature" : "mztEsPesQ3OGqhYh2nv1iYPZdsYPUY8bFt5AzbxnbLYwUI5r+tOeQem1slr8LydSATRN0aZfqHW5rl4FXuLLQhtkuhZ2qOony8H6kKTGGf5QXWCIPN/ge3V1hfB+GNpAd2UNg9XqLt3PKZHrlW4aLLHixCUf0Baf/6lfQhHLHyp/HcVjm0VCp1s3MSMBZaFHoQTNoBf/Ur7xUvwYH2OvQNMn854yRnlpa85VI/RABZ7...
"SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem", 
"UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:041977924901:test:5b4ab24c-a248-4b55-84ef-7143a86f483f" 
} 
END

my $tampered_body_sns = AWS::SNS::Verify->new(body => $tampered_body, certificate_string => $cert_string);
throws_ok(
    sub { $tampered_body_sns->verify },
    #qr/Could not verify the SES message/,
    'Ouch',
    "Tampered with body doesn't valiate",
);



note "Invalid cert doesn't validate";

my $invalid_cert_sns = AWS::SNS::Verify->new(body => $body, certificate_string => "Nopes");
throws_ok(
    sub { $invalid_cert_sns->verify },
    qr/FATAL: invalid or unsupported RSA key format/,
    "Invalid cert doesn't valiate",
);



done_testing();

lib/AWS/SNS/Verify.pm  view on Meta::CPAN

use Data::Structure::Util;

has body => (
    is          => 'ro',
    required    => 1,
);

has message => (
    is          => 'ro',
    lazy        => 1,
    default     => sub {
        my $self = shift;
        return JSON::decode_json($self->body);
    }
);

has certificate_string => (
    is          => 'ro',
    lazy        => 1,
    default     => sub {
        my $self = shift;
        return $self->fetch_certificate;
    }
);

has certificate => (
    is          => 'ro',
    lazy        => 1,
    default     => sub {
        my $self = shift;
        return Crypt::PK::RSA->new(\$self->certificate_string);
    }
);

has validate_signing_cert_url => (
    is      => 'ro',
    lazy    => 1,
    default => 1,
);

sub fetch_certificate {
    my $self = shift;
    my $url = $self->valid_cert_url($self->message->{SigningCertURL});
    my $response = HTTP::Tiny->new->get($url);
    if ($response->{success}) {
        return $response->{content};
    }
    else {
        ouch $response->{status}, $response->{reason}, $response;
    }
}

sub generate_signature_string {
    my $self = shift;
    my $body = $self->message;
    my @fields;
    if ($body->{Type} eq 'Notification') {
        @fields = (qw(Message MessageId Subject Timestamp TopicArn Type)) ;
    }
    else {
        @fields = (qw(Message MessageId SubscribeURL Timestamp Token TopicArn Type));
    }
    my @parts;
    foreach my $field (@fields) {
        if (exists $body->{$field}) {
            push @parts, $field;
            push @parts, $body->{$field};
        }
    }
    return join("\n", @parts)."\n";
}

sub decode_signature {
    my $self = shift;
    return decode_base64($self->message->{Signature});
}

sub verify {
    my $self = shift;
    my $pk = $self->certificate;
    unless ($pk->verify_message($self->decode_signature, $self->generate_signature_string, 'SHA1', 'v1.5')) {
        ouch 'Bad SNS Signature', 'Could not verify the SNS message from its signature.', $self;
    }
    return 1;
}

# See also:
# https://github.com/aws/aws-php-sns-message-validator/blob/master/src/MessageValidator.php#L22
sub valid_cert_url {
    my $self = shift;
    my ($url_string) = @_;
    $url_string ||= '';

    return $url_string unless $self->validate_signing_cert_url;

    my $url = URI::URL->new($url_string);
    unless ( $url->can('host') ) {
        ouch 'Bad SigningCertURL', "The SigningCertURL ($url_string) isn't a valid URL", $self;
    }

lib/AWS/SNS/Verify.pm  view on Meta::CPAN

    # sns.cn-north-1.amazonaws.com.cn   (AWS China)
    my $dot = qr/\./;
    my $region = qr/[a-zA-Z0-9-]+/;
    unless ($host =~ /^ sns $dot $region $dot amazonaws $dot com(\.cn)? $/x) {
        ouch 'Bad SigningCertURL', "The SigningCertURL ($url_string) isn't an Amazon endpoint", $self;
    }

    return $url_string;
}

sub TO_JSON {
    my $self = shift;
    return unbless($self);
}

=head1 NAME

AWS::SNS::Verify - Verifies authenticity of SNS messages.

=head1 VERSION

t/01_verify.t  view on Meta::CPAN

    "MessageAttributes" : {
        "AWS.SNS.MOBILE.MPNS.Type" : {"Type":"String","Value":"token"},
        "AWS.SNS.MOBILE.WNS.Type" : {"Type":"String","Value":"wns/badge"},
        "AWS.SNS.MOBILE.MPNS.NotificationClass" : {"Type":"String","Value":"realtime"}
    }
}
END

my $tampered_body_sns = AWS::SNS::Verify->new(body => $tampered_body, certificate_string => $cert_string);
throws_ok(
    sub { $tampered_body_sns->verify },
    qr/Could not verify the SNS message/,
    "Tampered with body doesn't validate",
);



note "Invalid cert doesn't validate";

my $invalid_cert_sns = AWS::SNS::Verify->new(body => $body, certificate_string => "Nopes");
throws_ok(
    sub { $invalid_cert_sns->verify },
    qr/FATAL: invalid or unsupported RSA key format/,
    "Invalid cert doesn't valiate",
);



done_testing();

t/02_valid_cert_url.t  view on Meta::CPAN

use Test::Exception;

use lib '../lib';

use AWS::SNS::Verify;


my $sns = AWS::SNS::Verify->new(body => '');

throws_ok(
    sub { $sns->valid_cert_url(undef) },
    qr/\QThe SigningCertURL () isn't a valid URL/,
);

throws_ok(
    sub { $sns->valid_cert_url("abc") },
    qr/\QThe SigningCertURL (abc) isn't a valid URL/,
);


throws_ok(
    sub { $sns->valid_cert_url("http://my.bad.com/cert.pem") },
    qr|\QThe SigningCertURL (http://my.bad.com/cert.pem) isn't an Amazon endpoint|,
);



my $valid_euwest_url = "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a046b3aafc7f4149a.pem";
is(
    $sns->valid_cert_url($valid_euwest_url),
    $valid_euwest_url,
    "Valid url returns url",



( run in 0.241 second using v1.01-cache-2.11-cpan-a5abf4f5562 )