File Coverage

blib/lib/Net/Amazon/S3/Signature/V4Implementation.pm
Criterion Covered Total %
statement 147 151 97.3
branch 25 38 65.7
condition 13 19 68.4
subroutine 27 27 100.0
pod 2 3 66.6
total 214 238 89.9


line stmt bran cond sub pod time code
1             package Net::Amazon::S3::Signature::V4Implementation;
2             # ABSTRACT: Implements the Amazon Web Services signature version 4, AWS4-HMAC-SHA256 (copy of Net::Amazon::Signature::V4)
3             $Net::Amazon::S3::Signature::V4Implementation::VERSION = '0.99';
4              
5 99     99   815 use strict;
  99         293  
  99         3424  
6 99     99   596 use warnings;
  99         278  
  99         3296  
7 99     99   52608 use sort 'stable';
  99         63983  
  99         727  
8              
9 99     99   5470 use Digest::SHA qw/sha256_hex hmac_sha256 hmac_sha256_hex/;
  99         284  
  99         7124  
10 99     99   53939 use Time::Piece ();
  99         741399  
  99         3106  
11 99     99   951 use URI::Escape;
  99         301  
  99         6652  
12 99     99   720 use URI;
  99         291  
  99         2207  
13 99     99   596 use URI::QueryParam;
  99         254  
  99         250778  
14              
15             our $ALGORITHM = 'AWS4-HMAC-SHA256';
16             our $MAX_EXPIRES = 604800; # Max, 7 days
17              
18             our $X_AMZ_ALGORITHM = 'X-Amz-Algorithm';
19             our $X_AMZ_CONTENT_SHA256 = 'X-Amz-Content-Sha256';
20             our $X_AMZ_CREDENTIAL = 'X-Amz-Credential';
21             our $X_AMZ_DATE = 'X-Amz-Date';
22             our $X_AMZ_EXPIRES = 'X-Amz-Expires';
23             our $X_AMZ_SIGNEDHEADERS = 'X-Amz-SignedHeaders';
24             our $X_AMZ_SIGNATURE = 'X-Amz-Signature';
25              
26              
27              
28             sub new {
29 7     7 0 19 my $class = shift;
30 7         20 my ( $access_key_id, $secret, $endpoint, $service ) = @_;
31 7         34 my $self = {
32             access_key_id => $access_key_id,
33             secret => $secret,
34             endpoint => $endpoint,
35             service => $service,
36             };
37 7         16 bless $self, $class;
38 7         26 return $self;
39             }
40              
41              
42             sub sign {
43 1     1 1 3 my ( $self, $request ) = @_;
44              
45 1         5 $request = $self->_augment_request( $request );
46              
47 1         7 my $authz = $self->_authorization( $request );
48 1         6 $request->header( Authorization => $authz );
49 1         54 return $request;
50             }
51              
52              
53             sub sign_uri {
54 6     6 1 146 my ( $self, $uri, $expires_in, $for_method ) = @_;
55              
56 6         34 my $request = $self->_augment_uri( $uri, $expires_in, $for_method );
57              
58 6         23 my $signature = $self->_signature( $request );
59              
60 6         21 $uri = $request->uri;
61 6         52 my $query = $uri->query;
62 6         86 $uri->query( undef );
63 6         109 $uri = $uri . '?' . $self->_sort_query_string( $query );
64 6         25 $uri .= "&$X_AMZ_SIGNATURE=$signature";
65              
66 6         84 return $uri;
67             }
68              
69             # _headers_to_sign:
70             # Return the sorted lower case headers as required by the generation of canonical headers
71              
72             sub _headers_to_sign {
73 15     15   27 my $req = shift;
74              
75 15 100       66 my @headers_to_sign = $req->uri->query_param( $X_AMZ_SIGNEDHEADERS )
76             ? $req->uri->query_param( $X_AMZ_SIGNEDHEADERS )
77             : $req->headers->header_field_names
78             ;
79              
80 15         3746 return sort { $a cmp $b } map { lc } @headers_to_sign
  33         60  
  33         95  
81             }
82              
83             # _augment_request:
84             # Append mandatory header fields
85              
86             sub _augment_request {
87 1     1   3 my ( $self, $request ) = @_;
88              
89 1 50       4 $request->header($X_AMZ_DATE => $self->_format_amz_date( $self->_req_timepiece($request) ))
90             unless $request->header($X_AMZ_DATE);
91              
92 1 50       124 $request->header($X_AMZ_CONTENT_SHA256 => sha256_hex($request->content))
93             unless $request->header($X_AMZ_CONTENT_SHA256);
94              
95 1         136 return $request;
96             }
97              
98             # _augment_uri:
99             # Append mandatory uri parameters
100              
101             sub _augment_uri {
102 6     6   19 my ($self, $uri, $expires_in, $method) = @_;
103              
104 6   50     29 my $request = HTTP::Request->new( $method || GET => $uri );
105              
106 6 50       399 $request->uri->query_param( $X_AMZ_DATE => $self->_format_amz_date( $self->_now ) )
107             unless $request->uri->query_param( $X_AMZ_DATE );
108              
109 6 50       1105 $request->uri->query_param( $X_AMZ_ALGORITHM => $ALGORITHM )
110             unless $request->uri->query_param( $X_AMZ_ALGORITHM );
111              
112 6 50       1334 $request->uri->query_param( $X_AMZ_CREDENTIAL => $self->_credential( $request ) )
113             unless $request->uri->query_param( $X_AMZ_CREDENTIAL );
114              
115 6 50 33     1403 $request->uri->query_param( $X_AMZ_EXPIRES => $expires_in || $MAX_EXPIRES )
116             unless $request->uri->query_param( $X_AMZ_EXPIRES );
117 6 50       2409 $request->uri->query_param( $X_AMZ_EXPIRES => $MAX_EXPIRES )
118             if $request->uri->query_param( $X_AMZ_EXPIRES ) > $MAX_EXPIRES;
119              
120 6         900 $request->uri->query_param( $X_AMZ_SIGNEDHEADERS => 'host' );
121              
122 6         1771 return $request;
123             }
124              
125             # _canonical_request:
126             # Construct the canonical request string from an HTTP::Request.
127              
128             sub _canonical_request {
129 7     7   32 my ( $self, $req ) = @_;
130              
131 7         25 my $creq_method = $req->method;
132              
133 7 100       94 my ( $creq_canonical_uri, $creq_canonical_query_string ) =
134             ( $req->uri =~ m@([^?]*)\?(.*)$@ )
135             ? ( $1, $2 )
136             : ( $req->uri, '' );
137 7         183 $creq_canonical_uri =~ s@^https?://[^/]*/?@/@;
138 7         44 $creq_canonical_uri = _simplify_uri( $creq_canonical_uri );
139 7         24 $creq_canonical_query_string = $self->_sort_query_string( $creq_canonical_query_string );
140              
141             # Ensure Host header is present as its required
142 7 50       30 if (!$req->header('host')) {
143 7 100       370 my $host = $req->uri->_port
144             ? $req->uri->host_port
145             : $req->uri->host
146             ;
147 7         458 $req->header('Host' => $host);
148             }
149 7   100     378 my $creq_payload_hash = $req->header($X_AMZ_CONTENT_SHA256)
150             # Signed uri doesn't have content
151             || 'UNSIGNED-PAYLOAD';
152              
153             # There's a bug in AMS4 which causes requests without x-amz-date set to be rejected
154             # so we always add one if its not present.
155 7         374 my $amz_date = $req->header($X_AMZ_DATE);
156 7         291 my @sorted_headers = _headers_to_sign( $req );
157             my $creq_canonical_headers = join '',
158             map {
159 7         31 sprintf "%s:%s\x0a",
160             lc,
161 13         47 join ',', sort {$a cmp $b } _trim_whitespace($req->header($_) )
  0         0  
162             }
163             @sorted_headers;
164 7         31 my $creq_signed_headers = $self->_signed_headers( $req );
165 7         25 my $creq = join "\x0a",
166             $creq_method, $creq_canonical_uri, $creq_canonical_query_string,
167             $creq_canonical_headers, $creq_signed_headers, $creq_payload_hash;
168              
169 7         26 return $creq;
170             }
171              
172             # _string_to_sign
173             # Construct the string to sign.
174              
175             sub _string_to_sign {
176 7     7   18 my ( $self, $req ) = @_;
177 7         21 my $dt = $self->_req_timepiece( $req );
178 7         434 my $creq = $self->_canonical_request($req);
179 7         49 my $sts_request_date = $self->_format_amz_date( $dt );
180 7         284 my $sts_credential_scope = join '/', $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
181 7         285 my $sts_creq_hash = sha256_hex( $creq );
182              
183 7         31 my $sts = join "\x0a", $ALGORITHM, $sts_request_date, $sts_credential_scope, $sts_creq_hash;
184 7         26 return $sts;
185             }
186              
187             # _authorization
188             # Construct the authorization string
189              
190             sub _signature {
191 7     7   18 my ( $self, $req ) = @_;
192              
193 7         24 my $dt = $self->_req_timepiece( $req );
194 7         499 my $sts = $self->_string_to_sign( $req );
195 7         22 my $k_date = hmac_sha256( $dt->strftime('%Y%m%d'), 'AWS4' . $self->{secret} );
196 7         283 my $k_region = hmac_sha256( $self->{endpoint}, $k_date );
197 7         76 my $k_service = hmac_sha256( $self->{service}, $k_region );
198 7         64 my $k_signing = hmac_sha256( 'aws4_request', $k_service );
199              
200 7         88 my $authz_signature = hmac_sha256_hex( $sts, $k_signing );
201 7         25 return $authz_signature;
202             }
203              
204             sub _credential {
205 7     7   501 my ( $self, $req ) = @_;
206              
207 7         21 my $dt = $self->_req_timepiece( $req );
208              
209 7         553 my $authz_credential = join '/', $self->{access_key_id}, $dt->strftime('%Y%m%d'), $self->{endpoint}, $self->{service}, 'aws4_request';
210 7         258 return $authz_credential;
211             }
212              
213             sub _signed_headers {
214 8     8   22 my ( $self, $req ) = @_;
215              
216 8         22 my $authz_signed_headers = join ';', _headers_to_sign( $req );
217 8         26 return $authz_signed_headers;
218             }
219              
220             sub _authorization {
221 1     1   5 my ( $self, $req ) = @_;
222              
223 1         5 my $authz_signature = $self->_signature( $req );
224 1         5 my $authz_credential = $self->_credential( $req );
225 1         4 my $authz_signed_headers = $self->_signed_headers( $req );
226              
227 1         7 my $authz = "$ALGORITHM Credential=$authz_credential,SignedHeaders=$authz_signed_headers,Signature=$authz_signature";
228 1         5 return $authz;
229              
230             }
231              
232             sub _simplify_uri {
233 7     7   16 my $orig_uri = shift;
234 7         27 my @parts = split /\//, $orig_uri;
235 7         16 my @simple_parts = ();
236 7         18 for my $part ( @parts ) {
237 18 100 66     105 if ( ! length $part || $part eq '.' ) {
    50          
238             } elsif ( $part eq '..' ) {
239 0         0 pop @simple_parts;
240             } else {
241 12         29 push @simple_parts, $part;
242             }
243             }
244 7         28 my $simple_uri = '/' . join '/', @simple_parts;
245 7 50 66     33 $simple_uri .= '/' if $orig_uri =~ m@/$@ && $simple_uri !~ m@/$@;
246 7         60 return $simple_uri;
247             }
248             sub _sort_query_string {
249 13     13   52 my $self = shift;
250 13 100       33 return '' unless $_[0];
251 12         23 my @params;
252 12         44 for my $param ( split /&/, $_[0] ) {
253             my ( $key, $value ) =
254 60         148 map { tr/+/ /; uri_escape( uri_unescape( $_ ) ) } # escape all non-unreserved chars
  120         1064  
  120         238  
255             split /=/, $param;
256 60 50       1379 push @params, [$key, (defined $value ? $value : '')];
257             #push @params, [$key, $value];
258             }
259             return join '&',
260 60         218 map { join '=', grep defined, @$_ }
261 12 50       54 sort { ( $a->[0] cmp $b->[0] ) || ( $a->[1] cmp $b->[1] ) }
  108         235  
262             @params;
263             }
264             sub _trim_whitespace {
265 13     13   551 return map { my $str = $_; $str =~ s/^\s*//; $str =~ s/\s*$//; $str } @_;
  13         24  
  13         66  
  13         105  
  13         100  
266             }
267             sub _str_to_timepiece {
268 21     21   37 my $date = shift;
269 21 50       104 if ( $date =~ m/^\d{8}T\d{6}Z$/ ) {
270             # assume basic ISO 8601, as demanded by AWS
271 21         86 return Time::Piece->strptime($date, '%Y%m%dT%H%M%SZ');
272             } else {
273             # assume the format given in the AWS4 test suite
274 0         0 $date =~ s/^.{5}//; # remove weekday, as Amazon's test suite contains internally inconsistent dates
275 0         0 return Time::Piece->strptime($date, '%d %b %Y %H:%M:%S %Z');
276             }
277             }
278              
279             sub _format_amz_date {
280 14     14   464 my ($self, $dt) = @_;
281              
282 14         66 $dt->strftime('%Y%m%dT%H%M%SZ');
283             }
284              
285             sub _now {
286 7     7   333 return scalar Time::Piece->gmtime;
287             }
288              
289             sub _req_timepiece {
290 22     22   124 my ($self, $req) = @_;
291 22   100     83 my $x_date = $req->header($X_AMZ_DATE) || $req->uri->query_param($X_AMZ_DATE);
292 22   66     3488 my $date = $x_date || $req->header('Date');
293 22 100       109 if (!$date) {
294             # No date set by the caller so set one up
295 1         8 my $piece = $self->_now;
296 1         102 $req->date($piece->epoch);
297 1         271 return $piece
298             }
299 21         73 return _str_to_timepiece($date);
300             }
301              
302             1;
303              
304             __END__
305              
306             =pod
307              
308             =encoding UTF-8
309              
310             =head1 NAME
311              
312             Net::Amazon::S3::Signature::V4Implementation - Implements the Amazon Web Services signature version 4, AWS4-HMAC-SHA256 (copy of Net::Amazon::Signature::V4)
313              
314             =head1 VERSION
315              
316             version 0.99
317              
318             =head1 DESCRIPTION
319              
320             This package clones L<Net::Amazon::Signature::V4> 0.19 adding support for
321             signing URIs (GET request)
322              
323             Until https://github.com/Grinnz/Net-Amazon-Signature-V4/pull/5 will be merged
324             we have to maintain our clone.
325              
326             =head1 Net::Amazon::Signature::S4 AUTHORS
327              
328             Tim Nordenfur, C<< <tim at gurka.se> >>
329              
330             Maintained by Dan Book, C<< <dbook at cpan.org> >>
331              
332             =head2 sign( $request )
333              
334             Signs a request with your credentials by appending the Authorization header. $request should be an HTTP::Request. The signed request is returned.
335              
336             =head2 sign_uri( $uri, $expires_in?, $for_method? )
337              
338             Signs an uri with your credentials by appending the Authorization query parameters.
339              
340             C<< $expires_in >> integer value in range 1..604800 (1 second .. 7 days).
341              
342             C<< $expires_in >> default value is its maximum: 604800
343              
344             C<< $for_method >> HTTP method this uri should be signed for, default C<GET>
345              
346             The signed uri is returned.
347              
348             =head1 AUTHOR
349              
350             Branislav Zahradník <barney@cpan.org>
351              
352             =head1 COPYRIGHT AND LICENSE
353              
354             This software is copyright (c) 2021 by Amazon Digital Services, Leon Brocard, Brad Fitzpatrick, Pedro Figueiredo, Rusty Conover, Branislav Zahradník.
355              
356             This is free software; you can redistribute it and/or modify it under
357             the same terms as the Perl 5 programming language system itself.
358              
359             =cut