File Coverage

blib/lib/AWS/SNS/Verify.pm
Criterion Covered Total %
statement 58 65 89.2
branch 10 14 71.4
condition 2 2 100.0
subroutine 13 14 92.8
pod 5 5 100.0
total 88 100 88.0


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