File Coverage

blib/lib/Net/Amazon/SNS/Signature.pm
Criterion Covered Total %
statement 43 51 84.3
branch 7 12 58.3
condition 3 6 50.0
subroutine 12 14 85.7
pod 2 4 50.0
total 67 87 77.0


line stmt bran cond sub pod time code
1             package Net::Amazon::SNS::Signature;
2             $Net::Amazon::SNS::Signature::VERSION = '0.007';
3 2     2   135252 use strict; use warnings;
  2     2   5  
  2         60  
  2         11  
  2         5  
  2         58  
4              
5 2     2   11 use Carp;
  2         3  
  2         116  
6 2     2   1111 use Crypt::OpenSSL::RSA;
  2         12155  
  2         71  
7 2     2   926 use Crypt::OpenSSL::X509;
  2         3263  
  2         100  
8 2     2   873 use MIME::Base64;
  2         1134  
  2         102  
9 2     2   1168 use LWP::UserAgent;
  2         89424  
  2         1135  
10              
11             =head1 NAME
12              
13             Net::Amazon::SNS::Signature
14              
15             =head1 DESCRIPTION
16              
17             For the verification of Amazon SNS messages
18              
19             =head1 USAGE
20              
21             # Will download the signature certificate from SigningCertURL attribute of $message_ref
22             # use LWP::UserAgent
23             my $sns_signature = Net::Amazon::SNS::Signature->new();
24             if ( $sns_signature->verify( $message_ref ) ){ ... }
25              
26             # Will automatically download the certificate using your own user_agent ( supports ->get returns HTTP::Response )
27             my $sns_signature = Net::Amazon::SNS::Signature->new( user_agent => $my_user_agent );
28             if ( $sns_signature->verify( $message_ref ) ){ ... }
29              
30             # Provide the certificate yourself
31             my $sns_signature = Net::Amazon::SNS::Signature->new()
32             if ( $sns_signature->verify( $message_ref, $x509_cert ) ) { ... }
33              
34             =head2 verify
35              
36             Call to verify the message, C<$message_ref> is required as first parameter and should be
37             a hash ref, C<$x509_cert> is optional and should be a raw x509 certificate as downloaded
38             from Amazon.
39              
40             See L for
41             information on the content of a message
42              
43             Usage:
44              
45             my $is_verified = $this->verify({
46             Message => 'My Test Message',
47             MessageId => '4d4dc071-ddbf-465d-bba8-08f81c89da64',
48             Subject => 'My subject',
49             Timestamp => '2012-06-05T04:37:04.321Z',
50             TopicArn => 'arn:aws:sns:us-east-1:123456789012:s4-MySNSTopic-1G1WEFCOXTC0P',
51             Type => 'Notification',
52             Signature => 'EXAMPLElDMXvB8r9R83tGoNn0ecwd5UjllzsvSvbItzfaMpN2nk5HVSw7XnOn',
53             SigningCertURL => 'https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3e'
54             });
55              
56             =cut
57              
58             sub verify {
59 1     1 1 480 my ( $self, $message, $cert ) = @_;
60              
61             my $signature = MIME::Base64::decode_base64($message->{Signature})
62 1 50       11 or carp( "Signature is a required attribute of message" );
63 1         3 my $string = $self->build_sign_string( $message );
64             my $public_key = $cert ? $self->_key_from_cert( $cert ) :
65 1 50       4 $self->_public_key_from_url( $message->{SigningCertURL} );
66              
67 1         13 my $rsa = Crypt::OpenSSL::RSA->new_public_key( $public_key );
68 1         631 return $rsa->verify($string, $signature);
69             }
70              
71             =head2 build_sign_string
72              
73             Given a C<$message_ref> will return a formatted string ready to be signed.
74              
75             Usage:
76              
77             my $sign_string = $this->build_sign_string({
78             Message => 'Hello',
79             MessageId => '12345',
80             Subject => 'I am a message',
81             Timestamp => '2016-01-20T14:37:01Z',
82             TopicArn => 'xyz123',
83             Type => 'Notification'
84             });
85              
86             =cut
87              
88             sub build_sign_string {
89 1     1 1 3 my ( $self, $message ) = @_;
90              
91 1         2 my @keys = $self->_signature_keys( $message );
92 1   33     8 defined($message->{$_}) or carp( sprintf( "%s is required", $_ ) ) for @keys;
93 1         2 return join( "\n", ( map { ( $_, $message->{$_} ) } @keys ), "" );
  6         18  
94             }
95              
96             sub new {
97 1     1 0 526 my ( $class, $args_ref ) = @_;
98             return bless {
99 1 50       6 defined($args_ref->{user_agent}) ? ( user_agent => $args_ref->{user_agent} ) : ()
100             }, $class;
101             }
102              
103             sub _public_key_from_url {
104 0     0   0 my ( $self, $url ) = @_;
105 0         0 my $response = $self->user_agent->get( $url );
106 0         0 my $content = $response->decoded_content;
107 0         0 return $self->_key_from_cert( $content );
108             }
109              
110             sub _key_from_cert {
111 1     1   2 my ( $self, $cert ) = @_;
112 1         106 my $x509 = Crypt::OpenSSL::X509->new_from_string(
113             $cert, Crypt::OpenSSL::X509::FORMAT_PEM
114             );
115 1         55 return $x509->pubkey;
116             }
117              
118             sub user_agent {
119 0     0 0 0 my ( $self ) = @_;
120 0 0       0 unless ( defined( $self->{user_agent} ) ){
121 0         0 $self->{user_agent} = LWP::UserAgent->new();
122             }
123 0         0 return $self->{user_agent};
124             }
125              
126             sub _signature_keys {
127 6     6   119 my ( $self, $message ) = @_;
128 6         22 my @keys = qw/Message MessageId/;
129              
130 6 100 66     53 if ( $message->{Type} && $message->{Type} =~ m/\A(?:Subscription|Unsubscribe)Confirmation\z/ ){
131 2         8 push @keys, qw/SubscribeURL Timestamp Token/;
132             }
133             else {
134 4 100       15 push @keys, ( defined ( $message->{Subject} ) ? qw/Subject Timestamp/ : 'Timestamp' );
135             }
136              
137 6         80 return @keys, qw/TopicArn Type/;
138             }
139              
140             1;