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.005';
3 2     2   50306 use strict; use warnings;
  2     2   2  
  2         44  
  2         6  
  2         2  
  2         37  
4              
5 2     2   4 use Carp;
  2         5  
  2         84  
6 2     2   788 use Crypt::OpenSSL::RSA;
  2         9722  
  2         52  
7 2     2   762 use Crypt::OpenSSL::X509;
  2         2711  
  2         104  
8 2     2   803 use MIME::Base64;
  2         894  
  2         92  
9 2     2   1086 use LWP::UserAgent;
  2         57508  
  2         710  
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 => $message_ref, certificate => $x509_cert }) ) { ... }
33              
34             =head2 verify
35              
36             Call to verify the message, C<$message> is required as first parameter, C<$cert> is
37             optional and should be a raw x509 certificate as downloaded from Amazon.
38              
39             =cut
40              
41             sub verify {
42 1     1 1 403 my ( $self, $message, $cert ) = @_;
43              
44             my $signature = MIME::Base64::decode_base64($message->{Signature})
45 1 50       10 or carp( "Signature is a required attribute of message" );
46 1         3 my $string = $self->build_sign_string( $message );
47             my $public_key = $cert ? $self->_key_from_cert( $cert ) :
48 1 50       3 $self->_public_key_from_url( $message->{SigningCertURL} );
49              
50 1         12 my $rsa = Crypt::OpenSSL::RSA->new_public_key( $public_key );
51 1         570 return $rsa->verify($string, $signature);
52             }
53              
54             =head2 build_sign_string
55              
56             Given a C<$message_ref> will return a formatted string ready to be signed.
57              
58             Usage:
59              
60             my $sign_string = $this->build_sign_string({
61             Message => 'Hello',
62             MessageId => '12345',
63             Subject => 'I am a message',
64             Timestamp => '2016-01-20T14:37:01Z',
65             TopicArn => 'xyz123',
66             Type => 'Notification'
67             });
68              
69             =cut
70              
71             sub build_sign_string {
72 1     1 1 1 my ( $self, $message ) = @_;
73              
74 1         2 my @keys = $self->_signature_keys( $message );
75 1   33     10 defined($message->{$_}) or carp( sprintf( "%s is required", $_ ) ) for @keys;
76 1         1 return join( "\n", ( map { ( $_, $message->{$_} ) } @keys ), "" );
  6         9  
77             }
78              
79             sub new {
80 1     1 0 302 my ( $class, $args_ref ) = @_;
81             return bless {
82 1 50       5 defined($args_ref->{user_agent}) ? ( user_agent => $args_ref->{user_agent} ) : ()
83             }, $class;
84             }
85              
86             sub _public_key_from_url {
87 0     0   0 my ( $self, $url ) = @_;
88 0         0 my $response = $self->user_agent->get( $url );
89 0         0 my $content = $response->decoded_content;
90 0         0 return $self->_key_from_cert( $content );
91             }
92              
93             sub _key_from_cert {
94 1     1   1 my ( $self, $cert ) = @_;
95 1         107 my $x509 = Crypt::OpenSSL::X509->new_from_string(
96             $cert, Crypt::OpenSSL::X509::FORMAT_PEM
97             );
98 1         60 return $x509->pubkey;
99             }
100              
101             sub user_agent {
102 0     0 0 0 my ( $self ) = @_;
103 0 0       0 unless ( defined( $self->{user_agent} ) ){
104 0         0 $self->{user_agent} = LWP::UserAgent->new();
105             }
106 0         0 return $self->{user_agent};
107             }
108              
109             sub _signature_keys {
110 6     6   15 my ( $self, $message ) = @_;
111 6         10 my @keys = qw/Message MessageId/;
112              
113 6 100 66     30 if ( $message->{Type} && $message->{Type} =~ m/\A(?:Subscription|Unsubscribe)Confirmation\z/ ){
114 2         5 push @keys, qw/SubscribeURL Timestamp Token/;
115             }
116             else {
117 4 100       9 push @keys, ( defined ( $message->{Subject} ) ? qw/Subject Timestamp/ : 'Timestamp' );
118             }
119              
120 6         30 return @keys, qw/TopicArn Type/;
121             }
122              
123             1;