File Coverage

blib/lib/AWS/SNS/Verify.pm
Criterion Covered Total %
statement 61 70 87.1
branch 10 14 71.4
condition 2 2 100.0
subroutine 14 16 87.5
pod 5 6 83.3
total 92 108 85.1


line stmt bran cond sub pod time code
1 2     2   72823 use strict;
  2         14  
  2         57  
2 2     2   12 use warnings;
  2         4  
  2         105  
3             package AWS::SNS::Verify;
4             $AWS::SNS::Verify::VERSION = '0.0105';
5 2     2   1305 use JSON;
  2         20653  
  2         11  
6 2     2   1563 use HTTP::Tiny;
  2         99228  
  2         86  
7 2     2   958 use MIME::Base64;
  2         1210  
  2         115  
8 2     2   1065 use Moo;
  2         22690  
  2         10  
9 2     2   3873 use Ouch;
  2         5052  
  2         11  
10 2     2   1264 use Crypt::PK::RSA;
  2         32280  
  2         168  
11 2     2   886 use URI::URL;
  2         15971  
  2         108  
12 2     2   1044 use Data::Structure::Util;
  2         14236  
  2         1532  
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::PK::RSA->new(\$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 2     2 1 12 my $self = shift;
66 2         36 my $body = $self->message;
67 2         16 my @fields;
68 2 50       8 if ($body->{Type} eq 'Notification') {
69 2         6 @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 2         5 my @parts;
75 2         4 foreach my $field (@fields) {
76 12 50       25 if (exists $body->{$field}) {
77 12         19 push @parts, $field;
78 12         24 push @parts, $body->{$field};
79             }
80             }
81 2         500 return join("\n", @parts)."\n";
82             }
83              
84             sub decode_signature {
85 2     2 1 5 my $self = shift;
86 2         69 return decode_base64($self->message->{Signature});
87             }
88              
89             sub verify {
90 3     3 1 4717 my $self = shift;
91 3         67 my $pk = $self->certificate;
92 2 100       1439 unless ($pk->verify_message($self->decode_signature, $self->generate_signature_string, 'SHA1', 'v1.5')) {
93 1         9 ouch 'Bad SNS Signature', 'Could not verify the SNS message from its signature.', $self;
94             }
95 1         10 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 7352 my $self = shift;
102 6         15 my ($url_string) = @_;
103 6   100     21 $url_string ||= '';
104              
105 6 100       135 return $url_string unless $self->validate_signing_cert_url;
106              
107 5         59 my $url = URI::URL->new($url_string);
108 5 100       9129 unless ( $url->can('host') ) {
109 2         35 ouch 'Bad SigningCertURL', "The SigningCertURL ($url_string) isn't a valid URL", $self;
110             }
111 3         43 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         105 my $dot = qr/\./;
118 3         8 my $region = qr/[a-zA-Z0-9-]+/;
119 3 100       44 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         15 return $url_string;
124             }
125              
126             sub TO_JSON {
127 0     0 0   my $self = shift;
128 0           return unbless($self);
129             }
130              
131             =head1 NAME
132              
133             AWS::SNS::Verify - Verifies authenticity of SNS messages.
134              
135             =head1 VERSION
136              
137             version 0.0105
138              
139             =head1 SYNOPSIS
140              
141             my $body = request->body; # example fetch raw body from Dancer
142             my $sns = AWS::SNS::Verify->new(body => $body);
143             if ($sns->verify) {
144             return $sns->message;
145             }
146              
147             =head1 DESCRIPTION
148              
149             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.
150              
151             =head1 METHODS
152              
153             =head2 new
154              
155             Constructor.
156              
157             =over
158              
159             =item body
160              
161             Required. JSON string posted by AWS SNS. Looks like:
162              
163             {
164             "Type" : "Notification",
165             "MessageId" : "a890c547-5d98-55e2-971d-8826fff56413",
166             "TopicArn" : "arn:aws:sns:us-east-1:041977924901:foo",
167             "Subject" : "test subject",
168             "Message" : "test message",
169             "Timestamp" : "2015-02-20T20:59:25.401Z",
170             "SignatureVersion" : "1",
171             "Signature" : "kzi3JBQz64uFAXG9ZuAwPI2gYW5tT7OF83oeHb8v0/XRPsy0keq2NHTCpQVRxCgPOJ/QUB2Yl/L29/W4hiHMo9+Ns0hrqyasgUfjq+XkVR1WDuYLtNaEA1vLnA0H9usSh3eVVlLhpYzoT4GUoGgstRVvFceW2QVF9EYUQyromlcbOVtVpKCEINAvGEEKJNGTXQQUkPUka3YMhHitgQg1WlFBmf+oweSYUEj8+RoguWsn6vluxD0VtIOGOml5jlUecfhDqnetF5pUVYMqCHPfHn6RBguiW+XD6XWsdKKxkjqo90a65Nlb72gPSRw6+sIEIgf4J39WFZK+FCpeSm0qAg==",
172             "SigningCertURL" : "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-d6d679a1d18e95c2f9ffcf11f4f9e198.pem",
173             "UnsubscribeURL" : "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:041977924901:foo:20b2d060-2a32-4506-9cb0-24b8b9e605e1",
174             "MessageAttributes" : {
175             "AWS.SNS.MOBILE.MPNS.Type" : {"Type":"String","Value":"token"},
176             "AWS.SNS.MOBILE.WNS.Type" : {"Type":"String","Value":"wns/badge"},
177             "AWS.SNS.MOBILE.MPNS.NotificationClass" : {"Type":"String","Value":"realtime"}
178             }
179             }
180              
181             =item certificate_string
182              
183             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.
184              
185             If you wish to use a cached version, then pass it in.
186              
187             =item validate_signing_cert_url (default: true)
188              
189             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.
190              
191             Don't ever do this in any kind of Production environment.
192              
193             =back
194              
195             =head2 verify
196              
197             Returns a 1 on success, or die with an L on a failure.
198              
199             =head2 message
200              
201             Returns a hash reference of the decoded L that was passed in to the constructor.
202              
203             =head2 certificate_string
204              
205             If you want to cache the certificate in a local cache, then get it using this method.
206              
207              
208             =head2 decode_signature
209              
210             You should never need to call this, it decodes the base64 signature.
211              
212             =head2 fetch_certificate
213              
214             You should never need to call this, it fetches the signing certificate.
215              
216              
217             =head2 generate_signature_string
218              
219             You should never need to call this, it generates the signature string required to verify the request.
220              
221             =head2 valid_cert_url
222              
223             You should never need to call this, it checks the validity of the certificate signing URL per L
224              
225             =head1 REQUIREMENTS
226              
227             Requires Perl 5.12 or higher and these modules:
228              
229             =over
230              
231             =item *
232              
233             Ouch
234              
235             =item *
236              
237             JSON
238              
239             =item *
240              
241             HTTP::Tiny
242              
243             =item *
244              
245             MIME::Base64
246              
247             =item *
248              
249             Moo
250              
251             =item *
252              
253             Crypt::OpenSSL::RSA
254              
255             =item *
256              
257             Crypt::OpenSSL::X509
258              
259             =back
260              
261             =head1 SUPPORT
262              
263             =over
264              
265             =item Repository
266              
267             L
268              
269             =item Bug Reports
270              
271             L
272              
273             =back
274              
275              
276             =head1 AUTHOR
277              
278             JT Smith
279              
280             =head1 LEGAL
281              
282             AWS::SNS::Verify is Copyright 2015 Plain Black Corporation (L) and is licensed under the same terms as Perl itself.
283              
284             =cut
285              
286              
287             1;