File Coverage

blib/lib/AWS/SNS/Verify.pm
Criterion Covered Total %
statement 43 68 63.2
branch 6 14 42.8
condition 2 2 100.0
subroutine 11 15 73.3
pod 5 5 100.0
total 67 104 64.4


line stmt bran cond sub pod time code
1 1     1   75832 use strict;
  1         2  
  1         23  
2 1     1   4 use warnings;
  1         2  
  1         37  
3             package AWS::SNS::Verify;
4             $AWS::SNS::Verify::VERSION = '0.0103';
5 1     1   520 use JSON;
  1         9773  
  1         6  
6 1     1   694 use HTTP::Tiny;
  1         42113  
  1         49  
7 1     1   517 use MIME::Base64;
  1         642  
  1         52  
8 1     1   591 use Moo;
  1         11728  
  1         8  
9 1     1   1658 use Ouch;
  1         1938  
  1         4  
10 1     1   514 use Crypt::OpenSSL::RSA;
  1         5371  
  1         28  
11 1     1   381 use Crypt::OpenSSL::X509;
  1         1511  
  1         46  
12 1     1   374 use URI::URL;
  1         6613  
  1         607  
13              
14             has body => (
15             is => 'ro',
16             required => 1,
17             );
18              
19             has message => (
20             is => 'ro',
21             lazy => 1,
22             default => sub {
23             my $self = shift;
24             return JSON::decode_json($self->body);
25             }
26             );
27              
28             has certificate_string => (
29             is => 'ro',
30             lazy => 1,
31             default => sub {
32             my $self = shift;
33             return $self->fetch_certificate;
34             }
35             );
36              
37             has certificate => (
38             is => 'ro',
39             lazy => 1,
40             default => sub {
41             my $self = shift;
42             return Crypt::OpenSSL::X509->new_from_string($self->certificate_string);
43             }
44             );
45              
46             has validate_signing_cert_url => (
47             is => 'ro',
48             lazy => 1,
49             default => 1,
50             );
51              
52             sub fetch_certificate {
53 0     0 1 0 my $self = shift;
54 0         0 my $url = $self->valid_cert_url($self->message->{SigningCertURL});
55 0         0 my $response = HTTP::Tiny->new->get($url);
56 0 0       0 if ($response->{success}) {
57 0         0 return $response->{content};
58             }
59             else {
60 0         0 ouch $response->{status}, $response->{reason}, $response;
61             }
62             }
63              
64             sub generate_signature_string {
65 0     0 1 0 my $self = shift;
66 0         0 my $body = $self->message;
67 0         0 my @fields;
68 0 0       0 if ($body->{Type} eq 'Notification') {
69 0         0 @fields = (qw(Message MessageId Subject Timestamp TopicArn Type)) ;
70             }
71             else {
72 0         0 @fields = (qw(Message MessageId SubscribeURL Timestamp Token TopicArn Type));
73             }
74 0         0 my @parts;
75 0         0 foreach my $field (@fields) {
76 0 0       0 if (exists $body->{$field}) {
77 0         0 push @parts, $field;
78 0         0 push @parts, $body->{$field};
79             }
80             }
81 0         0 return join("\n", @parts)."\n";
82             }
83              
84             sub decode_signature {
85 0     0 1 0 my $self = shift;
86 0         0 return decode_base64($self->message->{Signature});
87             }
88              
89             sub verify {
90 0     0 1 0 my $self = shift;
91 0         0 my $rsa = Crypt::OpenSSL::RSA->new_public_key($self->certificate->pubkey);
92 0 0       0 unless ($rsa->verify($self->generate_signature_string, $self->decode_signature)) {
93 0         0 ouch 'Bad SNS Signature', 'Could not verify the SES message from its signature.', $self;
94             }
95 0         0 return 1;
96             }
97              
98             # See also:
99             # https://github.com/aws/aws-php-sns-message-validator/blob/master/src/MessageValidator.php#L22
100             sub valid_cert_url {
101 6     6 1 6784 my $self = shift;
102 6         14 my ($url_string) = @_;
103 6   100     20 $url_string ||= '';
104              
105 6 100       129 return $url_string unless $self->validate_signing_cert_url;
106              
107 5         52 my $url = URI::URL->new($url_string);
108 5 100       7721 unless ( $url->can('host') ) {
109 2         44 ouch 'Bad SigningCertURL', "The SigningCertURL ($url_string) isn't a valid URL", $self;
110             }
111 3         39 my $host = $url->host;
112              
113             # Match all regional SNS endpoints, e.g.
114             # sns..amazonaws.com (AWS)
115             # sns.us-gov-west-1.amazonaws.com (AWS GovCloud)
116             # sns.cn-north-1.amazonaws.com.cn (AWS China)
117 3         90 my $dot = qr/\./;
118 3         18 my $region = qr/[a-zA-Z0-9-]+/;
119 3 100       38 unless ($host =~ /^ sns $dot $region $dot amazonaws $dot com(\.cn)? $/x) {
120 1         6 ouch 'Bad SigningCertURL', "The SigningCertURL ($url_string) isn't an Amazon endpoint", $self;
121             }
122              
123 2         13 return $url_string;
124             }
125              
126              
127             =head1 NAME
128              
129             AWS::SNS::Verify - Verifies authenticity of SNS messages.
130              
131             =head1 VERSION
132              
133             version 0.0103
134              
135             =head1 SYNOPSIS
136              
137             my $body = request->body; # example fetch raw body from Dancer
138             my $sns = AWS::SNS::Verify->new(body => $body);
139             if ($sns->verify) {
140             return $sns->message;
141             }
142              
143             =head1 DESCRIPTION
144              
145             This module will parse a message from Amazon Simple Notification Service and validate its signature. This way you know the message came from AWS and not some third-party. More info here: L.
146              
147             =head1 METHODS
148              
149             =head2 new
150              
151             Constructor.
152              
153             =over
154              
155             =item body
156              
157             Required. JSON string posted by AWS SNS. Looks like:
158              
159             {
160             "Type" : "Notification",
161             "MessageId" : "a890c547-5d98-55e2-971d-8826fff56413",
162             "TopicArn" : "arn:aws:sns:us-east-1:041977924901:foo",
163             "Subject" : "test subject",
164             "Message" : "test message",
165             "Timestamp" : "2015-02-20T20:59:25.401Z",
166             "SignatureVersion" : "1",
167             "Signature" : "kzi3JBQz64uFAXG9ZuAwPI2gYW5tT7OF83oeHb8v0/XRPsy0keq2NHTCpQVRxCgPOJ/QUB2Yl/L29/W4hiHMo9+Ns0hrqyasgUfjq+XkVR1WDuYLtNaEA1vLnA0H9usSh3eVVlLhpYzoT4GUoGgstRVvFceW2QVF9EYUQyromlcbOVtVpKCEINAvGEEKJNGTXQQUkPUka3YMhHitgQg1WlFBmf+oweSYUEj8+RoguWsn6vluxD0VtIOGOml5jlUecfhDqnetF5pUVYMqCHPfHn6RBguiW+XD6XWsdKKxkjqo90a65Nlb72gPSRw6+sIEIgf4J39WFZK+FCpeSm0qAg==",
168             "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-d6d679a1d18e95c2f9ffcf11f4f9e198.pem",
169             "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:041977924901:foo:20b2d060-2a32-4506-9cb0-24b8b9e605e1",
170             "MessageAttributes" : {
171             "AWS.SNS.MOBILE.MPNS.Type" : {"Type":"String","Value":"token"},
172             "AWS.SNS.MOBILE.WNS.Type" : {"Type":"String","Value":"wns/badge"},
173             "AWS.SNS.MOBILE.MPNS.NotificationClass" : {"Type":"String","Value":"realtime"}
174             }
175             }
176              
177             =item certificate_string
178              
179             By default AWS::SNS::Verify will fetch the certificate string by issuing an HTTP GET request to C. The SigningCertURL in the message must be a AWS SNS endpoint.
180              
181             If you wish to use a cached version, then pass it in.
182              
183             =item validate_signing_cert_url (default: true)
184              
185             If you're using a fake SNS server in your local test environment, the SigningCertURL won't be an AWS endpoint. If so, set validate_signing_cert_url to 0.
186              
187             Don't ever do this in any kind of Production environment.
188              
189             =back
190              
191             =head2 verify
192              
193             Returns a 1 on success, or die with an L on a failure.
194              
195             =head2 message
196              
197             Returns a hash reference of the decoded L that was passed in to the constructor.
198              
199             =head2 certificate_string
200              
201             If you want to cache the certificate in a local cache, then get it using this method.
202              
203              
204             =head2 decode_signature
205              
206             You should never need to call this, it decodes the base64 signature.
207              
208             =head2 fetch_certificate
209              
210             You should never need to call this, it fetches the signing certificate.
211              
212              
213             =head2 generate_signature_string
214              
215             You should never need to call this, it generates the signature string required to verify the request.
216              
217             =head2 valid_cert_url
218              
219             You should never need to call this, it checks the validity of the certificate signing URL per L
220              
221             =head1 REQUIREMENTS
222              
223             Requires Perl 5.12 or higher and these modules:
224              
225             =over
226              
227             =item *
228              
229             Ouch
230              
231             =item *
232              
233             JSON
234              
235             =item *
236              
237             HTTP::Tiny
238              
239             =item *
240              
241             MIME::Base64
242              
243             =item *
244              
245             Moo
246              
247             =item *
248              
249             Crypt::OpenSSL::RSA
250              
251             =item *
252              
253             Crypt::OpenSSL::X509
254              
255             =back
256              
257             =head1 SUPPORT
258              
259             =over
260              
261             =item Repository
262              
263             L
264              
265             =item Bug Reports
266              
267             L
268              
269             =back
270              
271              
272             =head1 AUTHOR
273              
274             JT Smith
275              
276             =head1 LEGAL
277              
278             AWS::SNS::Verify is Copyright 2015 Plain Black Corporation (L) and is licensed under the same terms as Perl itself.
279              
280             =cut
281              
282              
283             1;